Skip to content

Support async hook routes and bind the request for FastAPI hook routes - #4027

Open
T4rk1n wants to merge 2 commits into
devfrom
feat/async-hook-routes
Open

T4rk1n wants to merge 2 commits into
devfrom
feat/async-hook-routes

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Hook routes (dash.hooks.route) didn't work as async def, and on the FastAPI backend they couldn't read the request at all. The same FastAPI issue also broke MCP (_mcp).

Found while porting the Plotly Cloud dev tools publish route to the Quart/FastAPI backends (plotly/plotly-cloud-extension#112), which currently needs workarounds for both.

Changes

  • Async hook routes: with_app_context_factory wrapped every hook route in a sync function, so an async def route returned 500 on Flask and Quart and failed on FastAPI. It now returns an async wrapper for coroutine functions (Flask needs flask[async]).
  • App context leak: both wrappers set app_context in the caller's context, so dash.get_app() kept returning that app on the calling thread after the request. The sync wrapper now sets it only inside the copied context; the async wrapper sets it in the awaiting task's context and resets it afterwards. (Setting it in a copied context doesn't work for coroutines: ctx.run(func) only creates the coroutine there, and its body runs in the awaiting task's context.)
  • FastAPI middleware: it only bound the current request for _dash-* paths, so request_adapter() raised "No active request in context" in hook routes and in MCP. It now also handles exact-match routes registered through Dash (app.routes).
  • FastAPI body replay: the middleware reads the JSON body up front, which left the body stream empty for a handler reading it itself (the request hung). It now replays the body it read to the handler.
  • Tests: hook_cleanup reset hooks._ns["route"] instead of "routes", so hook routes leaked between tests.

Test plan

  • New tests/backend_tests/test_hook_routes.py: sync and async hook routes on Flask, Quart and FastAPI, reading JSON through the request adapter; get_app() returns the app serving the request with two apps; a FastAPI handler reading its own body. 4 of these fail without the fix.
  • tests/backend_tests + tests/integration/test_hooks.py: 47 passed, 1 skipped
  • tests/unit: 716 passed; the 3 failures also fail on dev locally (no Firefox, flask-compress installed, port 8050 subprocess test)
  • black 22.3.0, flake8, pylint clean on changed files
  • CI

🤖 Generated with Claude Code

T4rk1n and others added 2 commits September 30, 2026 15:59
- `with_app_context_factory` returns an async wrapper for `async def`
  hook routes, which previously failed on every backend.
- Both wrappers now set the app context only for the call instead of
  leaking it to the caller's context.
- The FastAPI middleware also handles routes registered through Dash
  (hook routes, MCP), so `request_adapter()` works for them, and replays
  the JSON body it read so handlers reading the body don't hang.
- Fix `hook_cleanup` resetting `"route"` instead of `"routes"`.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Dash performance benchmarks

⚠️ regressions to review

scenario metric p90 (ms) median growth baseline p90 note
⚠️ full_children_replace replace_ms 4365.6 1549.2 19.78x 4697.1 1.4x baseline (norm)
✅ callback_chain chain_ms 325.7 314.9 0.92x 499.1
✅ callback_chain graph_ms 1.6 1.6 1.0x 2.4
✅ callback_fanout fanout_ms 64.5 60.6 0.92x 92.5
✅ deep_nesting render_ms 41.0 39.1 0.95x 56.8
✅ initial_render_large render_ms 495.9 417.9 1.12x 694.4
✅ initial_render_small render_ms 71.6 63.2 1.0x 104.0
✅ patch_append_nested append_ms 102.6 72.1 2.34x 192.3
✅ patch_append_toplevel append_ms 93.9 64.1 2.16x 140.2
✅ patch_scalar_update_large update_ms 117.9 107.6 1.0x 202.9
✅ wildcard_all_resolve wildcard_ms 217.3 212.2 0.97x 313.6
✅ wildcard_all_resolve graph_ms 0.8 0.8 1.0x 1.3

growth = late-third / early-third per-op time; ~1 is flat, a large value means the per-op cost scales with accumulated state.

machine scale vs baseline: 0.66x - divided out of the baseline ratios so they compare like for like (the absolute warn/fail ceilings are left un-scaled); calibrated on initial_render_small.

@T4rk1n
T4rk1n marked this pull request as ready for review October 2, 2026 16:20
@T4rk1n
T4rk1n requested a review from camdecoster October 2, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant