feat: surface unprocessable state deltas as fatal client errors - #6827
feat: surface unprocessable state deltas as fatal client errors#6827FarhanAliRaza wants to merge 4 commits into
Conversation
When the backend sends a delta with a substate the frontend has no dispatch function for (mismatched frontend/backend state definitions), the frontend now: - validates the entire delta before dispatching anything, so a bad substate no longer partially applies an update or silently drops queued events, - logs an actionable error to the browser console, - reports the error to the backend via a new client_error socket event so it shows up in the terminal where devs look first, - treats the mismatch as fatal per reflex-dev#6019: no further events are sent until the frontend is rebuilt/reloaded, instead of erroring again on every interaction. Unexpected errors while applying a delta are likewise reported to the backend instead of vanishing as unhandled rejections. The backend on_client_error handler validates the payload shape, sanitizes and truncates client-supplied strings before logging, and only logs at error level for sockets with a linked token. Error type strings are shared via constants.ClientErrorType, and emit_update gained debug logging of outgoing substates (guarded by is_debug so the hot path is unaffected). Fixes reflex-dev#6019
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThis PR surfaces frontend state-delta processing failures to the backend and makes dispatch mismatches fatal.
Confidence Score: 4/5The PR is not yet safe to merge because the shared reporting budget can hide another session's sole fatal state-mismatch report. Any socket with a client-provided token can consume the process-wide 20-report window, after which an unrelated dispatch-mismatch report is discarded even though the affected frontend enters a fatal state and does not retry it. Files Needing Attention: reflex/app.py
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/.templates/web/utils/state.js | Adds pre-dispatch delta validation, fatal mismatch handling, and backend reporting for frontend processing errors. |
| reflex/app.py | Adds sanitized, rate-limited handling of frontend error reports through the configured frontend exception handler. |
| packages/reflex-base/src/reflex_base/constants/event.py | Defines the shared client-error socket event and error-type constants. |
| tests/units/test_client_error.py | Covers client-error formatting, sanitization, malformed inputs, unknown sessions, and reporting limits. |
Reviews (4): Last reviewed commit: "fix: route client_error reports through ..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cfd1341892
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
- Rate-limit error-level client_error logging to 5 entries per SID (cleared on disconnect) so a client that links an arbitrary token cannot flood backend logs. - Escape rich markup in sanitized client values; unescaped closing tags raised MarkupError and styling tags could inject into terminal logs. - Keep sanitized values within max_length including the truncation suffix. - Clear the event queue on fatal state mismatch; callers drain the queue in while-loops that would otherwise spin forever. - Await queueEvents inside the event handler try block so failures are reported via client_error instead of unhandled rejections; guard error.message for non-Error throws. - Add news fragments for the changelog check.
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Per-SID budgets reset when a new socket connects, so scripted reconnect loops could still flood backend logs. Add a process-wide time-window cap (20 entries per 60s) on top of the per-SID limit; later windows log again, so long-lived sessions are not silenced forever.
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Fix all with cubic | Re-trigger cubic
masenf
left a comment
There was a problem hiding this comment.
if we're introducing a new client error message type on the transport, then we should wire up the existing frontend error handler to this mechanism.
- Wire on_client_error into app.frontend_exception_handler so custom handlers (e.g. error trackers) receive client-reported errors. - Reword the frontend mismatch message to suggest refreshing the page first, per review. - Remove the per-update substate debug log (too spammy for --loglevel debug). - Warn once per window when the client_error rate limit trips so suppression is never silent.
|
Wired the client_error mechanism into the existing frontend error handler in ba8beb3: |
Summary
When the backend sends a delta the frontend cannot process (no dispatch function registered for a substate), the app currently throws
dispatch[substate] is not a functionin the browser console and otherwise looks silently broken. This PR makes that failure loud, actionable, and visible in the backend terminal — where Python devs look first.Fixes #6019. Supersedes #6128, rebuilt on top of the
packages/reflex-baselayout with the review findings from that PR addressed.Frontend (
reflex_base/.templates/web/utils/state.js)update.events.client_errorsocket event.backend_state_mismatchflag stops all further event sending and drops incoming updates, so the error is reported once instead of on every interaction. A page reload (e.g. after rebuilding the frontend) resets it.Backend (
reflex/app.py)EventNamespace.on_client_errorhandler logs frontend-reported errors in the terminal with remediation steps (rebuild frontend / checkapi_url).emit_updategained debug logging of outgoing substates, guarded byconsole.is_debug()so the hot path doesn't pay for message construction.Shared constants
SocketEvent.CLIENT_ERRORand a newClientErrorTypenamespace inreflex_base.constants.eventkeep the error-type strings in one place, matched by theERROR_TYPE_*constants instate.js.Testing
tests/units/test_client_error.pycovers both error branches, malformed payloads (which previously raisedAttributeError), unknown-sid gating, and sanitization/truncation.tests/units/test_app.pyandtests/units/utils/test_token_manager.pypass alongside (164 passed).