-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Fix socket reference shape mismatch in queueEvents #6094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix socket reference shape mismatch in queueEvents #6094
Conversation
Add resolveSocket() helper to handle both ref objects ({ current: Socket })
and raw sockets, fixing a bug where queueEvents would call socket.current
on an already-unwrapped socket, causing processEvent to bail out.
Greptile SummaryThis PR fixes a type inconsistency in the The fix introduces a
Key Changes:
Impact: Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant React as React Component
participant useEventLoop as useEventLoop Hook
participant queueEvents as queueEvents
participant processEvent as processEvent
participant applyEvent as applyEvent
participant queueEventIfSocketExists as queueEventIfSocketExists
participant resolveSocket as resolveSocket (NEW)
Note over React: socket = useRef(null)
Note over React: socket.current = Socket instance
React->>queueEvents: queueEvents(events, socket, ...)
Note over queueEvents: socket is REF { current: Socket }
queueEvents->>resolveSocket: resolveSocket(socket)
resolveSocket-->>queueEvents: Returns socket.current (raw Socket)
queueEvents->>processEvent: processEvent(raw Socket, ...)
processEvent->>applyEvent: applyEvent(event, raw Socket, ...)
Note over applyEvent: Handles _clear_local_storage event
applyEvent->>queueEventIfSocketExists: queueEventIfSocketExists(initialEvents(), raw Socket, ...)
Note over queueEventIfSocketExists: socket is now RAW Socket
queueEventIfSocketExists->>queueEvents: queueEvents(events, raw Socket, ...)
Note over queueEvents: socket is RAW Socket { connected: true, ... }
queueEvents->>resolveSocket: resolveSocket(socket)
resolveSocket-->>queueEvents: Returns socket (already raw)
queueEvents->>processEvent: processEvent(raw Socket, ...)
Note over queueEvents,processEvent: FIX: resolveSocket handles both shapes<br/>Without fix: socket.current would be undefined
|
Greptile found no issues!From now on, if a review finishes and we haven't found any issues, we will not post anything, but you can confirm that we reviewed your changes in the status check section. This feature can be toggled off in your Code Review Settings by deselecting "Create a status check for each PR". |
masenf
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with comments addressed, we can take this as a bug fix. but i think there are probably more systemic improvements that are still needed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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.