Repository navigation
Detect frontend/backend state tree mismatch on connect - #7553
Conversation
… 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
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
💡 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".
|
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
Merging this PR will not alter performance
|
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.
…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.
All Submissions:
Type of change
New Feature Submission:
Changes To Core Features:
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):
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.state_mismatchevent before hydration.Frontend (packages/reflex-base/src/reflex_base/.templates/web/utils/state.js):
stateSchema, the hash of the state tree the frontend was built for.authpayload under thestate_schemakey on every connect and reconnect.handleStateMismatch()to respond to the backend'sstate_mismatchevent:Compiler (reflex/compiler/compiler.py):
compile_contexts()to pass the state schema hash to the context template.Constants (packages/reflex-base/src/reflex_base/constants/):
STATE_MISMATCHsocket event andCONNECT_AUTH_STATE_SCHEMAcompile 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