Fix socket reference shape mismatch in queueEvents #6094
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Bug Report: Socket Reference Shape Mismatch in Event Queue
Summary
A type inconsistency in Reflex's frontend event processing causes storage-related events to fail and fall back to a slower recovery path. This adds unnecessary latency to cookie removal and localStorage/sessionStorage operations. The proposed fix eliminates silent failures, improves latency for these operations, and makes the event queue self-sufficient rather than dependent on implicit recovery.
Background
The Event Queue System
Reflex processes events one at a time to maintain order and consistency:
Socket References in React
React uses "refs" to hold mutable values that persist across renders:
This distinction between
socket(the ref) andsocket.current(the actual socket) is where the bug occurs.Why Use a Ref for the Socket?
In React, component functions re-run on every render. If you stored the socket in a regular variable, it would be recreated each time:
Refs solve this by providing a stable container that persists across renders:
The ref object
{ current: Socket }stays the same across renders, whilesocket.currentholds the actual WebSocket connection.Why Is the Socket Sometimes a Ref and Sometimes Raw?
This inconsistency arises from how event processing is structured:
Path 1: Initial Event Processing (Uses Ref)
When React's event loop processes events, it passes the ref:
Path 2: Event Handler Chain (Passes Raw Socket Along)
When
processEventcallsapplyEvent, it passes the raw socket it received:Path 3: Client-Side Events Queue More Events (Expects Ref)
When handling events like
_clear_local_storage, the code queues follow-up events:Path 4: queueEvents Assumes Ref (BUG!)
Finally,
queueEventsassumes it received a ref:Visual Summary of the Problem
The bug is a shape mismatch: the code at the end of the chain expects a ref, but receives a raw socket because the socket was "unwrapped" earlier in the chain and never re-wrapped.
The Bug
Problem Statement
The
queueEventsfunction sometimes receives a socket ref (an object with.current) and sometimes receives the raw socket object directly. The code assumes it always receives a ref:When
socketis already the raw socket object:socket.currentisundefinedprocessEvent(undefined, ...)is calledCode Location
File:
reflex/.templates/web/utils/state.jsBuggy Code (lines 470-471):
When This Happens
The bug triggers when handling any of these 5 client-side storage/cookie events:
_remove_cookierx.remove_cookies()_clear_local_storagerx.clear_local_storage()_remove_local_storagerx.remove_local_storage()_clear_session_storagerx.clear_session_storage()_remove_session_storagerx.remove_session_storage()All of these events need to trigger a rehydration after clearing data (so the UI reflects the cleared state). They do this by queuing
initialEvents():Regular events (like button clicks, form submits, etc.) do NOT trigger this bug because they don't call
queueEventIfSocketExists. They go directly to the backend via WebSocket without queuing follow-up events.The call chain is:
processEvent(socket, ...)— socket is raw (already resolved)applyEvent(event, socket, ...)— socket is still rawqueueEventIfSocketExists(..., socket, ...)— socket is still rawqueueEvents(..., socket, ...)— socket is still rawprocessEvent(socket.current, ...)— BUG! socket.current is undefinedThe Recovery Mechanism
A
useEffecthook runs on every React render and drains the event queue:This recovery mechanism masks the bug by eventually processing the stuck events.
Evidence
Reproduction Steps
processEventbails outTest Application
Instrumentation Patch
To observe the bug, add this instrumentation to
state.js:Observed Console Output
This proves:
socketis the raw Socket object (not a ref)socket.currentisundefinedprocessEventbails outImpact Analysis
What Happens When the Bug Triggers
_clear_local_storageevent is handledinitialEvents()(rehydration) is queuedprocessEvent(undefined, ...)is calledPractical User Impact
The recovery mechanism runs on the next React render, which typically happens within:
This delay is generally not perceptible to users (human perception threshold ~100-200ms).
When Impact Could Be Higher
Why Fix Now?
.currenton a non-ref objectThe Fix
Solution
Add a helper function that handles both socket shapes:
Then use it in
queueEvents:Diff
Why This Fix is Safe
Report generated from deterministic simulation testing.