Raise when an event handler is awaited outside any client's event

Merged
MA
2h ago
+667-28

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Found in the reflex 0.11.0a1 pre-release testing (FINDING-010).

Stacked on #7796 (merge that first). Until #7796 merges, this PR's diff also contains its commits. The change here is the last commit, 83a304318.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

What was wrong. _RootMiddleware attaches the event processor's root EventContext to every request, and that context's token is "". execute_event_inline only raised when no context existed at all. So an API route that ran await State.handler() outside app.modify_state enqueued the event under the empty token. Every caller of that route shared one state: in the pre-release run, a counter route returned 1, 2, 3, 4 to callers with different tokens, and each caller read the previous caller's stored value, also across workers on prod + Redis. Browser sessions were not exposed, because a socket is never linked to an empty token.

The fix. A context without a token now counts as no context. Awaiting raises the existing EventHandlerTypeError: "Event handler … can only be awaited from an event handler or inside app.modify_state; use it as an event trigger or yield it instead." Inside app.modify_state, the context is forked to the token being modified, so awaiting there still works. That matches the rule docs/events/chaining_events.md already states ("awaiting a handler anywhere else raises an error"). The docs bullet now names the API-route case.

Tests (tests/units/reflex_base/event/processor/test_base_state_processor.py):

  • test_await_from_a_request_outside_any_event_raises: a request goes through _RootMiddleware with the real processor's root context. await State.add(1) raises, and the empty-token state is unchanged. This test failed before the fix.
  • test_await_inside_app_modify_state_runs_on_that_token: awaiting inside app.modify_state(BaseStateToken(...)) returns the handler's values and writes to that token's state.
  • The existing tests still cover awaiting inside a foreground or background event handler.

Checks:

  • uv run pytest tests/units --cov (without the Chromium-only reflex_bench/drivers/test_browser.py): 15665 passed, 66 skipped, 81.44% coverage.
  • ruff check and ruff format are clean.
  • pyright reflex tests reports 0 errors.

Generated by Claude Code