Skip to content

Detect frontend/backend state tree mismatch on connect - #7553

Merged
masenf merged 2 commits into
mainfrom
farhan/eloquent-brahmagupta-dhrcr0
Oct 14, 2026
Merged

masenf merged 2 commits into
mainfrom
farhan/eloquent-brahmagupta-dhrcr0

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

New Feature Submission:

  • Does your submission pass the tests?
  • Have you linted your code locally prior to submission?

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Description

This PR adds detection of frontend/backend state tree mismatches when the frontend connects to the backend. The compiled frontend and backend can come from different builds (e.g., a backend deploy while a browser keeps an old page, or two backend versions behind a load balancer during a rolling deploy). Reflex now detects this incompatibility and handles it gracefully.

Key Changes

Backend (reflex/state.py, reflex/app.py):

  • Added BaseState._to_frontend_schema() method that computes a stable hash of the state tree structure (state names, frontend vars with types, and event handler names). Defaults and values are excluded so trees are compatible when structurally equivalent.
  • The hash is cached and invalidated when any state in the tree is modified.
  • On socket connect, the backend compares the frontend's schema hash (sent in the CONNECT auth payload) against its own. If they mismatch, it emits a state_mismatch event before hydration.

Frontend (packages/reflex-base/src/reflex_base/.templates/web/utils/state.js):

  • The compiled context module now exports stateSchema, the hash of the state tree the frontend was built for.
  • The frontend sends this hash in the socket.io CONNECT auth payload under the state_schema key on every connect and reconnect.
  • Added handleStateMismatch() to respond to the backend's state_mismatch event:
    • In production: Reloads the page once per cooldown window (10 seconds). If a reload already happened within the cooldown, shows a "new version available" banner with a reload button instead, preventing reload loops when the same mismatch persists (e.g., cached bundle or multiple backend versions).
    • In development: Only logs a warning, allowing hot reload to work without interruption.

Compiler (reflex/compiler/compiler.py):

  • Updated compile_contexts() to pass the state schema hash to the context template.

Constants (packages/reflex-base/src/reflex_base/constants/):

  • Added STATE_MISMATCH socket event and CONNECT_AUTH_STATE_SCHEMA compile variable.

Testing

  • Added comprehensive unit tests in tests/units/test_state.py:

    • test_frontend_schema_is_stable_across_processes: Verifies the hash is deterministic across Python interpreters.
    • test_frontend_schema_ignores_defaults: Confirms defaults don't affect the hash.
    • test_frontend_schema_changes_with_tree_vars_and_handlers: Tests that structural changes (renaming states/vars, adding/removing handlers) invalidate the hash.
    • test_frontend_schema_changes_when_state_added: Verifies dynamic state registration invalidates the cache.
    • test_frontend_schema_leaves_out_backend_vars: Confirms backend-only vars don't affect the hash.
  • Added unit tests in tests/units/test_app.py:

    • test_on_connect_reports_state_schema_mismatch: Verifies the backend detects and logs mismatches, emits the event, and sanitizes client-supplied hashes.

https://claude.ai/code/session_01EYJS7NPEz1gNkXZdqQDTA3

View guided diff Turn on auto-fix

… a reload

The compiled frontend and the backend can come from different builds: a
redeploy with a page kept open, mixed backend versions behind a load
balancer, or an api_url pointing at another backend. The #7435 fallback
skips deltas for unknown substates, but the skew stays hidden from users
and a changed var in a known state is never detected.

BaseState._to_frontend_schema() hashes what the compiled frontend depends
on over the whole tree: each state's full name, its frontend var names and
types (base and computed), and its event handler names. Defaults and values
are left out. It is lru-cached like _to_schema() and cleared with it, so a
state registered later invalidates the cached root. The compiler writes the
hash into the context module as stateSchema; the frontend sends it in the
socket.io CONNECT auth under state_schema on the first connect and every
reconnect; EventNamespace.on_connect compares it with the backend's hash,
emits one state_mismatch event to that client, logs one warning, and still
hydrates. A missing hash (old frontend) is ignored, and an old backend
ignores the new auth key.

On a mismatch in prod the page reloads once, guarded by the existing
reflex_last_reloaded_on_error cooldown, so mixed versions cannot loop; a
mismatch inside the cooldown shows a banner with a reload button. In dev
only a console warning is logged, since a backend hot reload can reconnect
before Vite serves the rebuilt bundle.

Closes #7552.

Claude-Session: https://claude.ai/code/session_01EYJS7NPEz1gNkXZdqQDTA3
@FarhanAliRaza
FarhanAliRaza requested review from a team and Alek99 as code owners October 13, 2026 16:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T16:29:24.706386Z 5216375 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

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.

All reported issues were addressed across 16 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread reflex/app.py
Comment thread packages/reflex-base/news/+state-schema-mismatch.feature.md Outdated
Comment thread tests/units/test_state.py Outdated
Comment thread tests/units/test_app.py Outdated

@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: 5216375e5d

ℹ️ 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/compiler/compiler.py
Comment thread reflex/state.py Outdated
Comment thread reflex/state.py Outdated
@greptile-apps

greptile-apps Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[High impact] Adds state schema detection on frontend connect.

The PR still needs fixes for missed model-field changes and repeated reloads on slow pages before merging.

Summary

Adds a state-tree hash to the compiled frontend and compares it when the browser connects.

  • Reflex detects when a page and backend use different state trees.

Reviews (2) · Last reviewed commit: "Merge main into the state mismatch detec..." · Reviewed by Greptile

Comment thread reflex/state.py Outdated
Comment thread reflex/state.py Outdated
Comment thread packages/reflex-base/src/reflex_base/.templates/web/utils/state.js
Resolve the conflicts with the final form of #7435: the frontend now skips
an unknown substate instead of stopping, so the connect-level JS tests move
to the client_state_updates.mjs harness main added, the Playwright module
keeps main's renamed tests next to the new dev and prod mismatch tests, and
the docs fold the detection into main's state mismatch subsection.

Review findings addressed:
- Key the frontend schema cache by the registration context as well as the
  root class: apps in one process run on forked contexts that share the root
  State but not its substates. Regression test:
  test_frontend_schema_is_per_registration_context.
- Raise the reflex-base floor to 0.10.1.dev0: reflex now needs the
  state_schema template parameter and the new constants.
- Scope the on_connect log assertions to the reflex.app logger at WARNING.
- Also drop a test-defined substate from RegistrationContext.base_states.
- Say in the reflex-base news fragment that the reload and banner are
  production behavior and that development only warns.

Claude-Session: https://claude.ai/code/session_01EYJS7NPEz1gNkXZdqQDTA3
@codspeed

codspeed Bot commented Oct 13, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 9 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 148 untouched benchmarks


Comparing farhan/eloquent-brahmagupta-dhrcr0 (de3aa1d) with main (73c7cb3)

Open in CodSpeed

@masenf masenf added the on deck PRs lined up to review / merge next label Oct 13, 2026
@masenf
masenf merged commit 02b92bb into main Oct 14, 2026
149 checks passed
@masenf
masenf deleted the farhan/eloquent-brahmagupta-dhrcr0 branch October 14, 2026 08:27
@linear-code

linear-code Bot commented Oct 15, 2026

Copy link
Copy Markdown

ENG-13358

benedikt-bartscher added a commit to benedikt-bartscher/reflex that referenced this pull request Oct 17, 2026
Ported upstream changes into the resolver-aware code:

- Context template: hydrate_and_load (reflex-dev#7064) gets a resolved wire name in
  InternalNames (replacing hydrate), alongside upstream's state schema and
  initial state hashes. state.js reads app.hydrate_and_load and
  app.update_vars_internal instead of hardcoded default names, and the
  handshake query (token + scheme) is built for the warm-up socket too.
- State reads use upstream's useStateContext (reflex-dev#7441), keyed by the
  state's resolved full name, into the branch's prefixed local.
- Upstream made unknown substates non-fatal (reflex-dev#7435) and dropped
  backend_state_mismatch; the fatal minification scheme mismatch keeps its
  own flag (backend_scheme_mismatch) gating events, updates and reconnects.
- The frontend state schema hash (reflex-dev#7553) hashes resolver-independent names;
  the scheme digest already covers the wire names.
- The processor's hydrate/hydrate_and_load/on_load_internal lookups cache
  handlers, not names, so they follow a resolver installed later.
- BaseStateToken._state_key and client storage application
  (_apply_client_storage_vars) use default names and wire var keys.
- get_substate keeps upstream's loop but only lets the first segment name
  the state itself; get_name/get_full_name stay function-wide caches so a
  resolver install clears every class, and also clears _get_substate_path.
- on_connect sets up per-SID state before upstream's new first await.
- pyStrAt avoids Array.prototype.at, which the state.js tests forbid.
benedikt-bartscher added a commit to benedikt-bartscher/reflex that referenced this pull request Oct 17, 2026
…inification

Ported upstream changes into the resolver-aware code:

- Context template: hydrate_and_load (reflex-dev#7064) gets a resolved wire name in
  InternalEventNames (replacing hydrate), alongside upstream's state schema
  and initial state hashes. state.js reads app.hydrate_and_load and
  app.update_vars_internal instead of hardcoded default names, and the
  handshake query (token + scheme) is built for the warm-up socket too.
- State reads use upstream's useStateContext (reflex-dev#7441), keyed by the
  state's resolved full name, into the prefixed local.
- Upstream made unknown substates non-fatal (reflex-dev#7435) and dropped
  backend_state_mismatch; the fatal minification scheme mismatch keeps its
  own flag (backend_scheme_mismatch) gating events, updates and reconnects.
- The frontend state schema hash (reflex-dev#7553) hashes resolver-independent names;
  the scheme digest already covers the wire names.
- The processor's hydrate/hydrate_and_load/on_load_internal lookups cache
  handlers, not names, so they follow a resolver installed later.
- BaseStateToken._state_key keys by the resolver-independent name.
- get_substate keeps upstream's loop but only lets the first segment name
  the state itself; get_name/get_full_name stay function-wide caches so a
  resolver install clears every class, and also clears _get_substate_path.
- on_connect sets up per-SID state before upstream's new first await.
- pyStrAt avoids Array.prototype.at, which the state.js tests forbid.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

on deck PRs lined up to review / merge next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants