feat: surface unprocessable state deltas as fatal client errors - #6827
feat: surface unprocessable state deltas as fatal client errors#6827FarhanAliRaza wants to merge 3 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 makes unprocessable frontend state deltas fatal and reports client-side processing failures to backend logs.
Confidence Score: 4/5The PR is not yet safe to merge because an untrusted socket client can exhaust the shared log budget and hide another session's fatal state-mismatch report. Arbitrary client-provided tokens are sufficient to obtain linked SIDs, and all such SIDs consume one shared 20-entry window; once consumed, the backend silently drops the only report emitted by a frontend entering the fatal mismatch state. Files Needing Attention: reflex/app.py; packages/reflex-base/src/reflex_base/.templates/web/utils/state.js
|
| Filename | Overview |
|---|---|
| packages/reflex-base/src/reflex_base/.templates/web/utils/state.js | Adds pre-dispatch delta validation, fatal mismatch handling, and backend reporting; its one-shot mismatch report makes reliable backend delivery important. |
| reflex/app.py | Adds sanitized client-error logging and rate limits, but the shared limiter allows one client to suppress diagnostics from unrelated sessions. |
| packages/reflex-base/src/reflex_base/constants/event.py | Adds matching socket-event and client-error-type constants. |
| reflex/state.py | Documents the frontend dispatch-function requirement on StateUpdate deltas. |
| tests/units/test_client_error.py | Covers sanitization and limiter bounds but encodes the global cap without checking that unrelated legitimate sessions retain diagnostic visibility. |
Reviews (3): Last reviewed commit: "fix: bound client_error logging across r..." | 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.
| if self._client_error_window_count >= self._MAX_CLIENT_ERRORS_PER_WINDOW: | ||
| return |
There was a problem hiding this comment.
Shared budget suppresses mismatch reports
When a client opens sockets with arbitrary token query parameters and emits 20 client_error events within the shared window, this return silently drops an unrelated session's only fatal dispatch-mismatch report, leaving the backend terminal without the actionable error this feature is intended to surface.
How this was verified: The connection handler links client-provided tokens directly to SIDs, all linked SIDs consume this shared counter, and a frontend entering the fatal mismatch state emits its report only once.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="reflex/app.py">
<violation number="1" location="reflex/app.py:2204">
P2: A long-lived client that emits five errors is permanently silenced: the per-SID early return runs before window expiry is evaluated, and expiry resets only the global count. Evaluate/reset the window first and renew per-SID budgets when it expires so later windows can report errors again.
(Based on your team's feedback about bounded client_error logs across SIDs and time windows.) [FEEDBACK_USED]</violation>
</file>
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
|
|
||
| # Also bound total entries per time window: per-SID budgets reset on | ||
| # reconnect, so they alone do not stop scripted reconnect loops. | ||
| now = time.monotonic() |
There was a problem hiding this comment.
P2: A long-lived client that emits five errors is permanently silenced: the per-SID early return runs before window expiry is evaluated, and expiry resets only the global count. Evaluate/reset the window first and renew per-SID budgets when it expires so later windows can report errors again.
(Based on your team's feedback about bounded client_error logs across SIDs and time windows.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At reflex/app.py, line 2204:
<comment>A long-lived client that emits five errors is permanently silenced: the per-SID early return runs before window expiry is evaluated, and expiry resets only the global count. Evaluate/reset the window first and renew per-SID budgets when it expires so later windows can report errors again.
(Based on your team's feedback about bounded client_error logs across SIDs and time windows.) </comment>
<file context>
@@ -2188,6 +2198,16 @@ async def on_client_error(self, sid: str, data: Any):
+
+ # Also bound total entries per time window: per-SID budgets reset on
+ # reconnect, so they alone do not stop scripted reconnect loops.
+ now = time.monotonic()
+ if now - self._client_error_window_start > self._CLIENT_ERROR_WINDOW_SECONDS:
+ self._client_error_window_start = now
</file context>
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).