Fix uncalled rx.toast hint and dropped-result warning lookup

Merged
MA
36m ago
+75-15

Status

The original change in this PR landed on main through the squash merge of the stacked #7821 (554eb10a6). The branch now merges main, so the diff holds only two follow-up fixes to that code:

  • Uncalled rx.toast hint. For an uncalled rx.toast or rx.toast.<type>, the "Did you mean to call it" hint named the internal Toaster.send_toast(...) / Toaster.toast_*(...) methods, and the tests asserted that text. _creator_reference now names a creator rx.<name> when reflex exports it under that name, such as rx.redirect(...). For any other creator defined by reflex, whose name is internal, the hint is "Did you mean to call it?" with no name. An app's own creator keeps its qualified name, such as _go_home(...).
  • Dropped-result warning lookup. _warn_dropped_result indexed app._registration_context.event_handlers[name], which raises KeyError in the done callback when the name is not in the app's registry. It now uses .get(name) and falls back to the event name. In a running app, the processor resolves the handler from the same registry, so this is a robustness fix only.

Test plan

  • test_returned_mistaken_event_raises_type_error: 3 toast cases now expect "Did you mean to call it?" / "Did you mean to call them?"; a new case returns an app's own uncalled creator and expects "Did you mean to call it: _go_home(...)?".
  • New test_dropped_result_warning_names_an_unregistered_event in tests/units/test_app.py. It and test_on_event_warns_once_in_dev_when_a_frontend_result_is_dropped assert the full warning message.
  • All 4 fail against main's source and pass with the fix. The 30 related targeted tests pass; ruff, pyright and pre-commit are clean.

News fragments: news/+dropped-result-warning-lookup.bugfix.md, packages/reflex-base/news/+uncalled-toast-hint.bugfix.md.

Original description (landed via #7821)

Fixes FINDING-015 from the reflex 0.11.0a1 pre-release testing (artifacts on branch claude/vibrant-pasteur-3921g3, prerelease-testing/runs/2026-11-11-v0.11.0a1/events_await/ and verify_evt/).

Defect

Since handlers may return a value to the backend code awaiting them (#7673), every non-event return value became the handler's result. That silently swallowed the mistakes 0.10.1 reported with TypeError: Your handler ... must only return/yield: None, Events or other EventHandlers referenced by their class ...: returning self.other_handler (meant type(self).other_handler), an uncalled event creator such as rx.redirect, or a list holding one. On 0.11.0a1 the chained handler never runs and nothing is logged or toasted.

Fix

The rule stays narrow on purpose, so handlers keep returning arbitrary values to their awaiting callers: only values that can never be a meaningful result raise.

  • base_state_processor.py: after _is_event_like rejects a value, _is_invalid_result flags (as before) an unawaited coroutine, plus
    • a handler function bound to its state or to a background task's state proxy (self.other_handler), recognised as a bound method whose __self__.event_handlers[name].fn is the function; and the stand-in a background handler binds to, now marked _reflex_bound_background_task;
    • a callable annotated to return an EventSpec (every rx.* event creator incl. rx.toast; reflex does not otherwise mark creators — an unannotated helper is treated as a result);
    • a list/tuple holding either.
      These go through the existing invalid-yield path: the delta is flushed, then the 0.10.1 TypeError naming the handler is raised (backend exception handler / error toast), for clicked, awaited and background handlers alike. The check only runs after _is_event_like failed and starts with callable(), so handlers returning None or events never reach it.
  • reflex/app.py EventNamespace.on_event: in dev mode only, when a handler triggered from the frontend returns a non-None value (other than []), warn once per handler that the value is dropped and only awaiting backend code receives it. on_event is the one place that owns the discarded future, so the check is exact; the dev flag is read once, so prod pays one attribute check per event.
  • Docs: one rules bullet and one alert sentence in docs/events/chaining_events.md.

Interaction with #7761

This PR is expected to merge before #7761 ("Stream event handler yield and return values to the frontend caller"). When rebasing #7761 onto it:

  • _is_result can become not _is_invalid_result(value) after its None/event checks (replacing callable(value) or iscoroutine(value) or _holds_events_or_coroutines(value), which would reject legitimate callable results).
  • The on_event dev warning must be skipped for events that carry a request id (their value is streamed to the frontend), and the docs/news wording "is dropped" should be narrowed accordingly.

Test plan

  • tests/units/reflex_base/event/processor/test_base_state_processor.py:
    • test_returned_mistaken_event_raises_type_error (14 cases: sync / async / sync-generator / background handlers returning self.target; self.bg_target; uncalled rx.redirect, rx.toast, rx.toast.success; [self.target]; (rx.call_script,); sync and async yields of self.target and rx.redirect; awaiting inline a handler that returns self.target) — each also asserts the target never ran.
    • test_returned_callable_that_is_not_an_event_resolves_future (4 cases: private helper, len, functools.partial, a Mock).
    • test_returned_event_reference_still_chains (3 cases).
  • tests/units/test_app.py: test_on_event_warns_once_in_dev_when_a_frontend_result_is_dropped (6 cases), test_on_event_does_not_watch_frontend_results_in_prod; test_on_event_uses_frontend_traceparent's fake enqueue now returns a future.
  • Failing before on unfixed main: 11 of the 14 mistaken-value cases (Failed: DID NOT RAISE TypeError for returns_bound_handler, async_returns_bound_handler, generator_returns_bound_handler, returns_bound_background_handler, background_returns_bound_handler, returns_uncalled_redirect, returns_uncalled_toast, returns_uncalled_toast_success, returns_list_holding_bound_handler, returns_tuple_holding_uncalled_call_script, awaits_bound_handler_return) and all 6 warning cases; the 3 yield cases and the result/chaining cases pass before and after as guards. test_handler_return_value_resolves_future is untouched and passes.
  • After: the two touched modules 633 passed; full unit suite 15663 passed, 66 skipped (the Playwright browser-driver module could not run locally for lack of a Chromium binary). ruff check, ruff format, pyright reflex tests, pre-commit on changed files: clean.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • This change requires a documentation update (included)

Known limits

  • Event-creator detection relies on the -> EventSpec return annotation; an unannotated creator or one returning EventSpec | None is treated as a result (and gets the dev warning when clicked).
  • An inherited handler accessed through a background StateProxy comes back as a functools.partial and is not recognised.
  • The dev warning covers events from the frontend only, not chained or background-enqueued ones.
  • The error keeps the exact 0.10.1 text (reads "Returned events of types <class 'function'>" for an uncalled rx.redirect).

News fragments: news/+mistaken-handler-return-values.bugfix.md and packages/reflex-base/news/+mistaken-handler-return-values.bugfix.md.


Generated by Claude Code