Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 40 additions & 16 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,23 +677,47 @@ export const connect = async (

// On each received message, queue the updates and events.
socket.current.on("event", async (update) => {
for (const substate in update.delta) {
dispatch[substate](update.delta[substate]);
// handle events waiting for `is_hydrated`
if (
substate === state_name &&
update.delta[substate]?.is_hydrated_rx_state_
) {
queueEvents(on_hydrated_queue, socket, false, navigate, params);
on_hydrated_queue.length = 0;
try {
for (const substate in update.delta) {
if (typeof dispatch[substate] !== "function") {
const errorMsg = `Cannot process state update: dispatch function for substate "${substate}" is not available. This usually indicates a mismatch between frontend and backend state definitions. Please rebuild the frontend or check that api_url is correct.`;
console.error(errorMsg);
// Emit error back to backend so it appears in terminal logs
socket.current.emit("client_error", {
message: errorMsg,
substate: substate,
error_type: "dispatch_function_missing",
Comment thread
timon0305 marked this conversation as resolved.
Outdated
});
Comment thread
timon0305 marked this conversation as resolved.
Outdated
throw new Error(errorMsg);
}
dispatch[substate](update.delta[substate]);
// handle events waiting for `is_hydrated`
if (
substate === state_name &&
update.delta[substate]?.is_hydrated_rx_state_
) {
queueEvents(on_hydrated_queue, socket, false, navigate, params);
on_hydrated_queue.length = 0;
}
}
}
applyClientStorageDelta(client_storage, update.delta);
if (update.final !== null) {
event_processing = !update.final;
}
if (update.events) {
queueEvents(update.events, socket, false, navigate, params);
applyClientStorageDelta(client_storage, update.delta);
if (update.final !== null) {
event_processing = !update.final;
}
if (update.events) {
queueEvents(update.events, socket, false, navigate, params);
}
} catch (error) {
console.error("Error processing state update:", error);
// If error wasn't already emitted above, emit it
if (error.message && !error.message.includes("dispatch function")) {
Comment thread
timon0305 marked this conversation as resolved.
Outdated
socket.current.emit("client_error", {
message: error.message,
error_type: "state_update_processing_error",
});
Comment thread
timon0305 marked this conversation as resolved.
Outdated
}
// Stop processing further updates to prevent cascading errors
event_processing = false;
}
});
socket.current.on("reload", async (event) => {
Expand Down
33 changes: 33 additions & 0 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2145,6 +2145,12 @@ async def emit_update(self, update: StateUpdate, token: str) -> None:
f"Attempting to send delta to disconnected client {token!r}"
)
return
# Log the substates being sent for debugging mismatches
if update.delta:
substates = list(update.delta.keys())
console.debug(
f"Emitting state update for substates: {substates} to client {token!r}"
)
# Creating a task prevents the update from being blocked behind other coroutines.
await asyncio.create_task(
self.emit(str(constants.SocketEvent.EVENT), update, to=socket_record.sid),
Expand Down Expand Up @@ -2246,6 +2252,33 @@ async def on_ping(self, sid: str):
# Emit the test event.
await self.emit(str(constants.SocketEvent.PING), "pong", to=sid)

async def on_client_error(self, sid: str, data: dict[str, Any]):
"""Handle errors reported by the frontend.

This allows frontend errors (especially state update processing errors)
to be visible in backend logs, improving debuggability.

Args:
sid: The Socket.IO session id.
data: The error data from the client.
"""
error_type = data.get("error_type", "unknown")
message = data.get("message", "No error message provided")
substate = data.get("substate")

# Log the error with details
if error_type == "dispatch_function_missing":
console.error(
f"[Frontend Error - SID: {sid}] State update failed: "
f"Substate '{substate}' dispatcher not found. "
f"This indicates a frontend/backend mismatch. "
f"Ensure 'reflex export' or rebuild was run after state changes."
)
else:
console.error(
f"[Frontend Error - SID: {sid}] {error_type}: {message}"
)

async def link_token_to_sid(self, sid: str, token: str):
"""Link a token to a session id.

Expand Down
17 changes: 16 additions & 1 deletion reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2798,7 +2798,22 @@ def create(cls, *children, **props) -> Component:
frozen=True,
)
class StateUpdate:
"""A state update sent to the frontend."""
"""A state update sent to the frontend.

The delta contains state changes keyed by substate name. Each substate must
have a corresponding dispatch function registered in the frontend. If the
frontend receives a delta with an unknown substate, it will:

1. Log a detailed error to the browser console
2. Emit a 'client_error' event back to the backend
3. Stop processing further updates to prevent cascading errors

This typically indicates a mismatch between frontend and backend state
definitions, which can occur if:
- The frontend was not rebuilt after state changes
- The api_url points to a different backend version
- Manual state manipulation created invalid substate keys
"""

# The state delta.
delta: Delta = dataclasses.field(default_factory=dict)
Expand Down