Skip to content

feat: surface unprocessable state deltas as fatal client errors - #6827

Open
FarhanAliRaza wants to merge 3 commits into
reflex-dev:mainfrom
FarhanAliRaza:fix/unprocessable-delta-client-error
Open

feat: surface unprocessable state deltas as fatal client errors#6827
FarhanAliRaza wants to merge 3 commits into
reflex-dev:mainfrom
FarhanAliRaza:fix/unprocessable-delta-client-error

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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 function in 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-base layout with the review findings from that PR addressed.

Frontend (reflex_base/.templates/web/utils/state.js)

  • The entire delta is validated before anything dispatches, so a bad substate can no longer partially apply an update or silently drop update.events.
  • On mismatch, the frontend logs an actionable error to the browser console and reports it to the backend via a new client_error socket event.
  • The mismatch is treated as fatal, as requested in Need a better error path when backend sends unprocessable delta #6019: a backend_state_mismatch flag 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.
  • Unexpected errors while applying a delta are caught and reported to the backend too, instead of vanishing as unhandled promise rejections.

Backend (reflex/app.py)

  • New EventNamespace.on_client_error handler logs frontend-reported errors in the terminal with remediation steps (rebuild frontend / check api_url).
  • Hardened against abuse, since any socket can emit this event: non-dict payloads are ignored, client-supplied strings are sanitized (control chars/ANSI escapes stripped) and truncated before logging, and only sockets with a linked token can produce error-level log entries — unknown sids are logged at debug level.
  • emit_update gained debug logging of outgoing substates, guarded by console.is_debug() so the hot path doesn't pay for message construction.

Shared constants

  • SocketEvent.CLIENT_ERROR and a new ClientErrorType namespace in reflex_base.constants.event keep the error-type strings in one place, matched by the ERROR_TYPE_* constants in state.js.

Testing

  • New tests/units/test_client_error.py covers both error branches, malformed payloads (which previously raised AttributeError), unknown-sid gating, and sanitization/truncation.
  • tests/units/test_app.py and tests/units/utils/test_token_manager.py pass alongside (164 passed).

Review in cubic

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
@FarhanAliRaza
FarhanAliRaza requested a review from a team as a code owner July 31, 2026 20:35
@codspeed-hq

codspeed-hq Bot commented Jul 31, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing FarhanAliRaza:fix/unprocessable-delta-client-error (23434de) with main (43b1e17)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes unprocessable frontend state deltas fatal and reports client-side processing failures to backend logs.

  • Validates complete deltas before dispatching and stops subsequent event processing after a state-definition mismatch.
  • Adds shared client-error event constants and a sanitized backend Socket.IO handler.
  • Adds per-session and process-wide log limiting plus regression coverage for malformed and excessive reports.

Confidence Score: 4/5

The 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

Security Review

The reconnect-resistant limiter can be exhausted by unauthenticated, token-bearing socket clients, suppressing the sole backend report from an unrelated frontend with a fatal state mismatch.

Important Files Changed

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

Comment thread reflex/app.py
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread reflex/app.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread reflex/app.py
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js
Comment thread reflex/app.py
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
Comment thread packages/reflex-base/src/reflex_base/constants/event.py
Comment thread reflex/app.py Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js Outdated
- 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread reflex/app.py
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.
Comment thread reflex/app.py
Comment on lines +2208 to +2209
if self._client_error_window_count >= self._MAX_CLIENT_ERRORS_PER_WINDOW:
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread reflex/app.py

# 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()

@cubic-dev-ai cubic-dev-ai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

View Feedback

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>
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Need a better error path when backend sends unprocessable delta

1 participant