NiceGUI Troubleshooting And Quality Evidence¶
Use this reference to identify the layer that owns a NiceGUI failure and the evidence needed to distinguish similar symptoms. It covers behavior verified against NiceGUI 3.16.0; browser, Quasar, Vue, FastAPI, Socket.IO, Uvicorn, and proxy behavior must also be checked against the versions deployed by the target application.
The companion interaction mechanics page defines normal lifecycle, validation, upload, refresh, timer, task, and transport behavior. Component mechanics covers Quasar props, events, slots, wrapper models, and frontend payload mapping. FastAPI and Uvicorn startup covers import identity, workers, reload, and deployment topology.
Diagnostic Index¶
| Symptom | Likely owner | Discriminating evidence |
|---|---|---|
| upload rejected before handler runs | Quasar QUploader or browser selection rules |
on_rejected fires; no upload POST reaches the server |
upload request returns 400 or 413 |
proxy, ASGI multipart parsing, NiceGUI upload route, or application validation | HTTP status and response body from the upload request; proxy and server logs |
| page shows a response-timeout error | async page construction before client connection | warning naming response_timeout; page builder timing and connected() boundary |
| update appears only after reload | wrong client context, unobserved plain mutation, or missing explicit update | target Client, element deletion state, outbox traffic, and wrapper update call |
| update reaches one tab but not another | private page element tree or process-local fan-out | client IDs, page paths, worker identity, and app.clients(path) iteration |
| updates disappear after a brief network interruption | client deleted after reconnect timeout or outbox replay unavailable | disconnect/delete timestamps, reconnect timeout, next message ID, and reload log |
| old query result replaces a newer one | concurrent async completion race | request generation, start/end timestamps, query identity, and publish order |
| all clients pause during one action | blocking work on the event loop | event-loop lag and stack or profile showing synchronous I/O or CPU work |
| callback runs repeatedly after navigation | duplicate timer, event subscription, or lifecycle registration | registration count, client IDs, delete handlers, and task names |
| user state leaks across tabs or users | incorrect storage scope or module-level mutable state | storage scope, session ID, tab ID, process ID, and object identity |
| URL changes but content or state does not | History API used without a route/content transition | pushState/replaceState call versus ui.navigate.to or sub-page routing |
| changed CSS or image remains stale | static cache lifetime or proxy/browser cache | response URL, Cache-Control, cache source in developer tools, and content version |
| exception is logged but no page feedback appears | exception occurred outside an active UI slot or after the client was deleted | exception handler invoked, current client/slot, task owner, and element state |
Start with the smallest boundary that can explain the symptom. Browser developer tools establish whether an event, upload, static request, or socket message crossed the network. Server logs establish whether the page, client, handler, task, or service received it. Durable data inspection establishes whether the accepted operation committed independently of the UI.
Upload Failures¶
ui.upload is a Quasar uploader backed by an element-specific NiceGUI POST route. The tagged Upload implementation resolves the client_id and element ID from the route before converting each Starlette upload to event.file.
Rejected Before Transfer¶
max_file_size, max_total_size, max_files, and the Quasar accept prop operate in the browser. A rejection at this stage calls on_rejected; it does not prove that the server would reject an equivalent direct request. An unexpected rejection commonly comes from MIME patterns, file-count state retained in the uploader queue, or size units that differ from the intended policy.
Useful evidence includes the selected file's browser-reported type and size, current queue contents, configured Quasar props, and whether on_begin_upload or a network request occurs. Reset the uploader queue only when clearing previous selections is the intended product behavior.
Transfer Or Multipart Failure¶
If the POST begins but the upload handler does not run, inspect the HTTP status before changing page code:
| Response | Common boundary |
|---|---|
404 |
stale or deleted element/client route, incorrect proxy prefix, or navigation during transfer |
400 |
malformed multipart body, missing client_id, missing element ID, or no matching uploader element |
413 |
reverse-proxy or ASGI request-size limit |
422 |
route or dependency validation outside the normal NiceGUI upload route |
5xx |
multipart conversion, temporary storage, application handler, or downstream service failure |
The tagged FileUpload conversion keeps small uploads in memory and spills larger ones to a temporary file after Starlette's MultiPartParser.spool_max_size. This is a buffering threshold, not an acceptance limit. Concurrency multiplies memory and temporary-disk pressure, so record file size, concurrent upload count, process memory, temporary filesystem capacity, and proxy limits together.
Accepted But Unsafe Or Corrupt¶
file.name is reduced to its basename by NiceGUI, and file.content_type comes from the request. Neither establishes safe content. Server-side acceptance should record the authoritative byte size and verify content signature, parser behavior, quota, authorization, and application-selected destination. Use an independently generated storage key and keep active user content off the main application origin.
When downstream parsing fails, distinguish transport completion from domain acceptance. A successful upload POST can still produce a rejected document. Preserve an operation ID or storage record so logs and user feedback identify the same attempt without logging file content or sensitive form fields.
Initial Page Response Failures¶
The tagged page wrapper gives async page construction response_timeout seconds, three by default, to finish or signal that it is waiting for the client connection. If neither happens, NiceGUI cancels the page task, deletes that client, logs a warning, and serves a terminal 500 page through a fresh client.
Increasing response_timeout can be appropriate for bounded, unavoidable initial construction, but it does not make long I/O responsive. The relevant timing split is:
- code before
await ui.context.client.connected()delays the initial HTTP response - code after that await runs with a connected browser and can progressively update the page
- synchronous blocking work in either phase can still stall the event loop
Capture elapsed time around dependencies, database calls, remote clients, serialization, and component construction. A timeout with low service latency may indicate a page builder waiting on a condition that itself requires the browser connection.
Synchronous page-builder exceptions and async exceptions raised before the response is built can render an app.on_page_exception page. That handler is synchronous in NiceGUI 3.16. A returned FastAPI Response bypasses normal page rendering. Do not assume a global app.on_exception handler can reconstruct a failed initial element tree.
Connection, Reconnect, And Deleted Clients¶
The tagged Client lifecycle separates a socket disconnect from deletion. On disconnect, NiceGUI invokes disconnect handlers and waits for reconnect_timeout; a successful handshake cancels deletion. If no connection returns, NiceGUI closes tab storage as needed, invokes delete handlers, removes elements and bindings, stops the outbox, and removes the client from Client.instances.
Stale Client Writes¶
Holding an element, slot, timer, or client in a long-lived object can outlive the page that created it. Writes after deletion trigger NiceGUI's deleted-client warning and cannot produce a valid browser update. Before publishing detached work, retain the intended client deliberately and check client.is_deleted or membership in the current client set. A durable job result should be written to durable state even when its original page no longer exists; a later page load can rehydrate it.
Do not treat on_disconnect as final resource disposal. It also runs for reconnectable interruptions. Page-owned cleanup belongs in on_delete; transport telemetry and reversible status belong in on_disconnect and on_connect.
Reconnect Replay And Reload¶
The tagged Outbox retains recent element updates and messages. During handshake, the browser provides the next message ID it expects. NiceGUI rewinds retained history and replays from that ID. If the ID is no longer available because of age or message_history_length, NiceGUI reloads the page.
This mechanism explains several superficially similar outcomes:
| Outcome | Interpretation |
|---|---|
| short interruption, state continues | client survived and required messages remained in history |
| interruption followed by reload | rewind target was unavailable or browser initiated a reload |
| interruption followed by fresh page state | original client was deleted and route rebuilt a new element tree |
| durable operation duplicated after reconnect | application command lacked idempotency; outbox replay is not a transaction protocol |
Correlate client ID, document or tab identity, message IDs, disconnect duration, reconnect timeout, and process ID. A load-balanced multi-worker deployment also needs compatible session affinity and shared application state; an in-memory client exists only in the worker that created it.
Missing Or Misrouted Updates¶
Each page client owns a private element tree. Mutating an element affects that element's client; mutating a plain list or model that has no active binding does not enqueue a browser update by itself. Check these in the owning layer:
- the element has not been deleted or replaced by a refresh
- the handler runs in the intended client's slot context
- the wrapper property is bindable or followed by its documented helper or
update() - the refreshable target belongs to the intended client
- application-wide producers iterate the intended
app.clients(path)and enter each client context - process-local events are not assumed to reach clients connected to another worker
A module-level @ui.refreshable can accumulate targets from several clients. Its tagged refresh() implementation clears and rebuilds every matching surviving target. Unexpected cross-client refresh therefore indicates target scope, not shared DOM. Unexpectedly missing refresh often indicates that the target container was deleted or the code refreshed a different decorated instance.
Async Races And Duplicate Actions¶
NiceGUI event handlers that return awaitables are scheduled as background tasks. Disabling a button reduces normal repeated clicks but does not serialize direct requests, reconnect replays, keyboard submission, another control, or another client.
Completion-Order Races¶
For replaceable reads such as search, an older request can finish after a newer request and overwrite its result. Record a generation or request key when work starts and compare it immediately before publishing. Cancellation can reduce wasted work but is not sufficient when the underlying thread, remote service, or database operation cannot be canceled.
For writes, define the service-level policy explicitly: lock, optimistic entity version, idempotency key, conflict response, or accepted duplicate semantics. UI busy state is feedback, not concurrency control.
Refresh Races¶
Each refreshable invocation owns a target container. Refresh clears that target before recreating children; concurrent refreshes can therefore interleave service reads and rendering. Await a refresh when the triggering action depends on completion, serialize refreshes for one target, or use a latest-generation policy for replaceable data. Keep long-lived loading and error indicators outside the cleared target if they must remain stable.
Observable Evidence¶
For an asynchronous interaction, logs are most useful when they contain operation ID, client ID, user or tenant identifier where safe, entity ID, request generation, start and finish time, outcome, and exception type. Avoid recording secrets, raw uploaded content, session cookies, or full form payloads.
Blocking Work And Event-Loop Lag¶
An async def callback does not make synchronous work non-blocking. CPU-heavy loops, synchronous HTTP clients, filesystem calls, image or document parsers, and blocking database drivers executed on the event loop delay socket heartbeats, all clients' event handlers, timers, page responses, and outbox delivery.
Use the execution boundary defined in interaction mechanics: non-blocking async APIs in the event loop, run.io_bound() for blocking I/O, run.cpu_bound() for serializable CPU work, or an external worker for durable jobs. A thread keeps the loop responsive but does not remove memory, timeout, thread-safety, or cancellation constraints. A process pool adds serialization and process-start constraints.
Evidence for event-loop blocking includes simultaneous latency across unrelated clients, delayed timers or Socket.IO heartbeats, event-loop lag metrics, and a stack or profile inside synchronous work. A single slow awaited network request that yields control does not by itself block other clients.
Timer, Listener, And Task Duplication¶
Repeated callbacks usually originate at registration, not dispatch. Common ownership mistakes include:
- creating
ui.timerrepeatedly during a refresh while retaining the old timer outside the cleared container - registering an application timer or lifecycle handler during a per-client page build
- subscribing a long-lived
Eventoutside a UI context without later unsubscribing - starting a new consumer task on every reconnect instead of once at application startup
- reloading a development process while an external scheduler still targets both old and new instances
In NiceGUI 3.16, a page-scoped ui.timer waits for its client connection and is canceled when its element is deleted. An app.timer is process-scoped. An Event subscription made inside a UI context is automatically removed on client deletion by default; one made outside UI context has no automatic client owner. Application lifecycle handlers and external broker consumers need an application-level owner and shutdown path.
Record timer or task name, registration site, process ID, client ID when applicable, activation state, and cancellation reason. Count registrations directly rather than inferring duplication from repeated business effects, which could also come from retries or multiple workers.
Storage And Navigation Drift¶
State drift often comes from assigning data to a scope with the wrong lifetime:
| Unexpected behavior | Scope to inspect |
|---|---|
| state disappears on reload or route navigation | app.storage.client or page-local Python object |
| state unexpectedly follows another tab | app.storage.user, browser, or module-global state |
| state is missing immediately after page construction | app.storage.tab accessed before client.connected() |
| state differs between workers | local file storage, in-memory tab state, or module-global state |
| browser storage mutation raises or is ignored | app.storage.browser changed after response construction |
The tagged storage implementation persists user and general scopes locally by default or in Redis when configured. Tab storage is in-memory unless Redis is configured. The signed browser cookie identifies a user storage record; storage scope is not authorization, and persisted identifiers must still be checked against the authenticated principal and tenant.
ui.navigate.to opens a route, client element anchor, or external URL. With ui.sub_pages, a relative same-app route can be handled within the current client. ui.navigate.history.push() and .replace() only change browser history state and the visible URL; they do not invoke a page builder or rehydrate content. A URL/content mismatch after pushState is therefore expected unless application code also owns the content transition.
A full navigation or reload creates a new page client, so page-local objects and client storage are not durable navigation state. Encode shareable state in route or query parameters, place tab- or user-lifetime state in the matching storage scope, and reload authoritative data from services rather than retaining element instances globally.
Static Assets, Media, And Cache Boundaries¶
The tagged Client.build_response() marks NiceGUI page and Markdown responses Cache-Control: no-store. A proxy that caches page HTML against that header can serve stale client IDs, initial state, or user-specific content and is misconfigured.
Static files intentionally use a different policy. In NiceGUI 3.16:
app.add_static_files()andapp.add_static_file()default toCache-Control: public, max-age=3600max_cache_age=0requests immediate revalidation behavior but does not create a private authorization boundary- media routes support byte-range streaming and should be used for seekable audio or video
- static and media directory helpers explicitly expose their contents without per-file application authorization
single_use=Trueremoves a route after the first handled request in one process; it is not a secure, distributed, or retry-safe download grant
The implementation is defined by app.add_static_* and app.add_media_* and CacheControlledStaticFiles.
For stale assets, inspect the actual response in browser developer tools: final URL after proxy rewriting, status, Cache-Control, ETag or modification metadata, service-worker involvement, and whether the response came from memory, disk, intermediary, or origin. Prefer content-versioned URLs for immutable assets. Query-string cache busting works only when every cache key includes the query and the origin serves the updated bytes.
Security-sensitive files belong behind an authenticated FastAPI route or object-store authorization mechanism with private cache policy. A hard-to-guess static URL is not access control, and public cache headers can retain content beyond logout or permission changes.
Exception Surfaces¶
NiceGUI exceptions have different user-feedback capabilities according to where they occur:
| Failure surface | Handler path | UI context available |
|---|---|---|
| page builder before response | app.on_page_exception, FastAPI handlers, then global exception handlers |
fresh error-page client for synchronous page handler |
| UI event or awaited callback in an element slot | client in-page exception handlers plus global handlers | originating slot while client remains alive |
| timer or NiceGUI background task | global handler; in-page handler only when task retained an active slot context | depends on captured context and client lifetime |
Event.emit() subscriber |
exception forwarded to global handling | subscriber's captured slot when available |
Event.call() subscriber |
exception propagates to caller | caller decides feedback and transaction behavior |
| FastAPI route outside NiceGUI page UI | FastAPI exception handling | no implicit NiceGUI element context |
The tagged app.handle_exception() first invokes a client's in-page exception handling when a client and slot are active, then invokes global exception handlers. Unexpected exceptions should retain a traceback and correlation ID in server logs. User feedback should be specific for expected domain failures and generic for unexpected failures, without exposing internals.
An exception notification is not recovery by itself. Restore busy state in finally, preserve user input after a recoverable failure, reconcile uncertain write outcomes from the authoritative store, and stop publishing to deleted clients.
Security-Sensitive Boundaries¶
The following client-visible mechanisms improve usability but do not enforce policy:
- disabled or hidden controls
- Quasar input rules and upload restrictions
- route names, element IDs, client IDs, or unguessable-looking static paths
- values retained in page, tab, browser, or user storage
- custom JavaScript validation or transformed event payloads
Authorization, tenant boundaries, accepted fields, type and range checks, optimistic concurrency, upload inspection, and durable write constraints belong on the server. For every mutation, identify the authenticated principal independently of browser-submitted ownership fields.
Do not interpolate untrusted values into raw ui.html, Vue templates, JavaScript, or style content. Use wrapper text/value APIs and structured serialization. Review proxy trust, forwarded-prefix configuration, cookies, origin exposure, and WebSocket policy as deployment inputs rather than component styling concerns.
Quality Evidence Matrix¶
A quality gate is satisfied by observable evidence, not by the presence of a pattern in source code.
| Concern | Required evidence |
|---|---|
| startup and import identity | application starts through the production entry point; reload and worker behavior match deployment; no duplicate module import paths |
| initial page response | representative pages stay within their response budget or intentionally cross client.connected() before long work |
| interaction correctness | primary actions, keyboard submission, validation failure, retry, duplicate action, and cancellation produce deterministic state |
| client lifecycle | disconnect/reconnect within the configured window preserves valid behavior; deletion releases page-owned resources |
| concurrency | stale reads cannot overwrite newer intent; writes have a documented conflict or idempotency policy |
| blocking behavior | concurrent-client check shows one slow action does not stall unrelated page events; profiles contain no unexpected event-loop blocking |
| storage isolation | reload, navigation, second-tab, second-user, process-restart, and multi-worker checks match each selected storage scope |
| uploads | browser rejection, direct server-side rejection, oversized request, invalid content, storage failure, and successful streaming path are distinguished |
| exception behavior | expected domain failures remain actionable; unexpected failures log tracebacks and correlation IDs; controls recover from busy state |
| cache behavior | page responses are no-store; public assets use deliberate versioning and lifetime; protected content is not exposed through public static routes |
| responsive layout | narrow mobile, intermediate, and wide desktop viewports show no clipping, overlap, inaccessible popup content, or layout shift from dynamic labels |
| accessibility | keyboard order, focus return, accessible names, validation association, contrast, reduced-motion behavior, and dialog/menu escape behavior are verified |
| observability | logs identify operation, client/process, route or entity, timing, and outcome without secrets or sensitive payloads |
| shutdown | timers, consumers, process/thread work, persistent storage, and external clients have deliberate cancellation or close behavior |
Testing Surfaces¶
NiceGUI's pytest integration provides two complementary fixtures:
Usersimulates interactions in Python and is the fast default for page content, component values, clicks, typing, event dispatch, navigation, and service-backed acceptance behavior.Screendrives a real headless browser and is reserved for behavior that depends on browser layout, JavaScript, actual uploads/downloads, focus, WebSockets, rendering, or client-side Quasar behavior.
Use lower-level tests for services, validation, authorization, idempotency, storage adapters, and task logic without constructing UI. Use User tests for application interaction contracts. Use a small set of Screen tests for the browser boundary, and supplement responsive or visual claims with screenshots and computed layout checks at explicit viewport sizes.
Tests should control async completion by observable state, events, or bounded timeouts rather than arbitrary sleeps. Reconnect, multi-tab, multi-user, and multi-worker behavior need dedicated environments because a single simulated client cannot establish those isolation claims.
Source Index¶
NiceGUI public documentation
NiceGUI 3.16.0 implementation
Related platform references