Bug Report
Summary
When using Dash with the FastAPI backend, accessing any non-Dash HTTP path (e.g. /login, /about, or any custom route) that falls through to the catchall route raises a RuntimeError: No active request in context, causing a 500 Internal Server Error.
Affected Version
- Dash version: 4.2.0+ (including latest
main branch as of 2026-06-11) - Python version: 3.12
- Backend: FastAPI
- OS: Linux
Description
The DashMiddleware in dash/backends/_fastapi.py only sets the request context (set_current_request) for Dash-specific routes (_dash-* paths and the root prefix). However, the _setup_catchall() method registers a {path:path} catch-all route that matches all GET paths and calls dash_app.index(), which internally requires a request context via get_current_request().
When a non-Dash path is requested, the middleware skips setting the request context and passes through. The catchall route then tries to render dash_app.index() without a context, triggering the RuntimeError.
Steps to Reproduce
- Create a Dash app with FastAPI backend:
fromfastapiimportFastAPIfromdashimportDash, htmlfromstarlette.testclientimportTestClientfastapi_app=FastAPI()
dash_app=Dash(__name__, server=fastapi_app)
dash_app.layout=html.Div("Hello World")
client=TestClient(fastapi_app)- Access the root path — works correctly:
resp=client.get("/")
assertresp.status_code==200# ✅ OK- Access any non-Dash path — crashes:
resp=client.get("/login")
# ❌ RuntimeError: No active request in contextExpected Behavior
The catchall route should successfully render the Dash index HTML for all paths, allowing Dash's client-side router to handle navigation (e.g., redirect to login page, show 404, etc.).
Actual Behavior
RuntimeError: No active request in context
Traceback (most recent call last):
File ".../fastapi/routing.py", line 134, in app
await wrap_app_handling_exceptions(app, request)(scope, receive, send)
...
File ".../dash/backends/_fastapi.py", line 333, in catchall
return Response(content=dash_app.index(), media_type="text/html")
^^^^^^^^^^^^^^^^
File ".../dash/dash.py", line 1269, in index
request = self.backend.request_adapter()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../dash/backends/_fastapi.py", line 837, in __init__
self._request: Request = get_current_request()
^^^^^^^^^^^^^^^^^^^^^
File ".../dash/backends/_fastapi.py", line 109, in get_current_request
raise RuntimeError("No active request in context")
Root Cause Analysis
In dash/backends/_fastapi.py, DashMiddleware.__call__ has the following flow (current latest code on main):
asyncdef__call__(self, scope, receive, send):
# ... lifespan / non-HTTP handling omitted ...# Non-Dash routes pass through WITHOUT setting request contextpath=scope["path"]
prefix=self.dash_app.config.routes_pathname_prefixdash_prefix=prefix.rstrip("/") +"/_dash-"if (
notpath.startswith(dash_prefix)
andpath!=prefixandpath!=prefix.rstrip("/")
):
awaitself.app(scope, receive, send) # ← no context set!return# Only Dash-specific routes get contextrequest=Request(scope, receive=receive)
token=set_current_request(request)
try:
# ... timing, hooks, etc ...finally:
reset_current_request(token)Meanwhile, _setup_catchall() registers a catch-all route:
def_setup_catchall(self):
dash_app=get_app()
asyncdefcatchall(_request: Request):
returnResponse(content=dash_app.index(), media_type="text/html")
# ^^^^^^^^^^^^^^^^^# dash_app.index() internally calls get_current_request()# which requires the context to be set by DashMiddlewareself.add_url_rule("{path:path}", catchall, methods=["GET"])The gap: When a non-Dash path (e.g. /login) is requested:
DashMiddleware sees it's not a Dash path → skips setting request context → passes through- FastAPI router matches the
catchall route → calls dash_app.index() dash_app.index() → get_current_request() → RuntimeError (no context was set)
Suggested Fix
Move Request creation and set_current_request()before the non-Dash path check, so that all HTTP requests have a request context available when the catchall route is reached:
asyncdef__call__(self, scope, receive, send):
# ... lifespan / non-HTTP handling omitted ...# Set request context for ALL HTTP requests (needed by catchall route# which can match non-Dash paths and still call dash_app.index())request=Request(scope, receive=receive)
token=set_current_request(request)
# Non-Dash routes pass through to avoid consuming body streampath=scope["path"]
prefix=self.dash_app.config.routes_pathname_prefixdash_prefix=prefix.rstrip("/") +"/_dash-"if (
notpath.startswith(dash_prefix)
andpath!=prefixandpath!=prefix.rstrip("/")
):
awaitself.app(scope, receive, send)
reset_current_request(token)
return# Dash route handling with timing, hooks, error handlingtry:
awaitself._setup_timing(request)
awaitself._run_before_hooks()
awaitself.app(scope, receive, send)
awaitself._run_after_hooks()
self._finalize_timing(request)
exceptExceptionase:
awaitself._handle_error(e, scope, receive, send)
finally:
reset_current_request(token)Key change: The Request object creation and set_current_request(request) call are moved above the non-Dash path check. This ensures that even when a request passes through to the catchall route, the request context is available for dash_app.index() to use.
Related Commit
The path check was improved in 6b2de3f (changing from "_dash-" in path to not path.startswith(dash_prefix)), but the underlying issue — missing request context for non-Dash paths that hit the catchall — was not addressed and remains in the latest main branch.
Additional Context
This bug manifests in real-world Dash applications that use client-side routing with the FastAPI backend. Pages like /login, /dashboard, /settings, etc. all trigger this error because they are not Dash-specific paths but still need to be served by the catchall route so that Dash's client-side router can take over.
Why this fix is safe for custom FastAPI routes (POST body stream)
The original code comment says "Non-Dash routes pass through to avoid consuming body stream". The concern was that Request(scope, receive=receive) might consume the request body, which would cause await request.json() in custom POST routes to hang (body already consumed).
This concern is unfounded.Request(scope, receive=receive) does not read the body. It only wraps the ASGI scope and receive callable. Body reading happens only when await request.body() or await request.json() is explicitly called.
In the suggested fix:
Request(scope, receive=receive) — does not read body, only wraps scopeset_current_request(request) — does not read body, only sets a context variable_setup_timing(request) → await request.json() — does read body, but only runs for Dash-specific routes
So custom FastAPI POST routes like @fastapi_app.post("/api/echo") are completely unaffected — the body stream remains available for the route handler to consume.
Verified with the following test case:
fromfastapiimportFastAPI, Requestfromfastapi.responsesimportJSONResponsefromdashimportDash, htmlfromstarlette.testclientimportTestClientfastapi_app=FastAPI()
dash_app=Dash(__name__, server=fastapi_app)
dash_app.layout=html.Div("Dash is running")
@fastapi_app.get("/api/echo")asyncdefecho_get():
returnJSONResponse({"method": "GET", "ok": True})
@fastapi_app.post("/api/echo")asyncdefecho_post(request: Request):
body=awaitrequest.json() # ← works correctly, body not consumed by middlewarereturnJSONResponse({"echo": body})
client=TestClient(fastapi_app)
assertclient.post("/api/echo", json={"msg": "hello"}).json() == {"echo": {"msg": "hello"}}
assertclient.get("/api/echo").json() == {"method": "GET", "ok": True}
assertclient.get("/login").status_code==200# catchall works, no RuntimeError
Bug Report
Summary
When using Dash with the FastAPI backend, accessing any non-Dash HTTP path (e.g.
/login,/about, or any custom route) that falls through to thecatchallroute raises aRuntimeError: No active request in context, causing a 500 Internal Server Error.Affected Version
mainbranch as of 2026-06-11)Description
The
DashMiddlewareindash/backends/_fastapi.pyonly sets the request context (set_current_request) for Dash-specific routes (_dash-*paths and the root prefix). However, the_setup_catchall()method registers a{path:path}catch-all route that matches all GET paths and callsdash_app.index(), which internally requires a request context viaget_current_request().When a non-Dash path is requested, the middleware skips setting the request context and passes through. The catchall route then tries to render
dash_app.index()without a context, triggering theRuntimeError.Steps to Reproduce
Expected Behavior
The catchall route should successfully render the Dash index HTML for all paths, allowing Dash's client-side router to handle navigation (e.g., redirect to login page, show 404, etc.).
Actual Behavior
Root Cause Analysis
In
dash/backends/_fastapi.py,DashMiddleware.__call__has the following flow (current latest code onmain):Meanwhile,
_setup_catchall()registers a catch-all route:The gap: When a non-Dash path (e.g.
/login) is requested:DashMiddlewaresees it's not a Dash path → skips setting request context → passes throughcatchallroute → callsdash_app.index()dash_app.index()→get_current_request()→ RuntimeError (no context was set)Suggested Fix
Move
Requestcreation andset_current_request()before the non-Dash path check, so that all HTTP requests have a request context available when the catchall route is reached:Key change: The
Requestobject creation andset_current_request(request)call are moved above the non-Dash path check. This ensures that even when a request passes through to the catchall route, the request context is available fordash_app.index()to use.Related Commit
The path check was improved in
6b2de3f(changing from"_dash-" in pathtonot path.startswith(dash_prefix)), but the underlying issue — missing request context for non-Dash paths that hit the catchall — was not addressed and remains in the latestmainbranch.Additional Context
This bug manifests in real-world Dash applications that use client-side routing with the FastAPI backend. Pages like
/login,/dashboard,/settings, etc. all trigger this error because they are not Dash-specific paths but still need to be served by the catchall route so that Dash's client-side router can take over.Why this fix is safe for custom FastAPI routes (POST body stream)
The original code comment says "Non-Dash routes pass through to avoid consuming body stream". The concern was that
Request(scope, receive=receive)might consume the request body, which would causeawait request.json()in custom POST routes to hang (body already consumed).This concern is unfounded.
Request(scope, receive=receive)does not read the body. It only wraps the ASGI scope and receive callable. Body reading happens only whenawait request.body()orawait request.json()is explicitly called.In the suggested fix:
Request(scope, receive=receive)— does not read body, only wraps scopeset_current_request(request)— does not read body, only sets a context variable_setup_timing(request)→await request.json()— does read body, but only runs for Dash-specific routesSo custom FastAPI POST routes like
@fastapi_app.post("/api/echo")are completely unaffected — the body stream remains available for the route handler to consume.Verified with the following test case: