Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
65 changes: 49 additions & 16 deletions reflex/.templates/web/utils/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ import { uploadFiles } from "$/utils/helpers/upload";
// Endpoint URLs.
const EVENTURL = env.EVENT;

// Socket event names (must match reflex/constants/event.py SocketEvent)
const CLIENT_ERROR_EVENT = "client_error";

// Client error types (must match backend error handling in app.py)
const ERROR_TYPE_DISPATCH_MISSING = "dispatch_function_missing";
const ERROR_TYPE_STATE_UPDATE = "state_update_processing_error";

// These hostnames indicate that the backend and frontend are reachable via the same domain.
const SAME_DOMAIN_HOSTNAMES = ["localhost", "0.0.0.0", "::", "0:0:0:0:0:0:0:0"];

Expand Down Expand Up @@ -677,23 +684,49 @@ 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;
let errorEmitted = false;
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_EVENT, {
message: errorMsg,
substate: substate,
error_type: ERROR_TYPE_DISPATCH_MISSING,
});
errorEmitted = true;
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);
// Emit error to backend if it wasn't already emitted
if (!errorEmitted) {
socket.current.emit(CLIENT_ERROR_EVENT, {
message: error.message || String(error),
error_type: ERROR_TYPE_STATE_UPDATE,
});
}
// 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
1 change: 1 addition & 0 deletions reflex/constants/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class SocketEvent(SimpleNamespace):

PING = "ping"
EVENT = "event"
CLIENT_ERROR = "client_error"

def __str__(self) -> str:
"""Get the string representation of the event name.
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