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:
from fastapi import FastAPI
from dash import Dash, html
from starlette.testclient import TestClient
fastapi_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("/")
assert resp.status_code == 200 # ✅ OK
- Access any non-Dash path — crashes:
resp = client.get("/login")
# ❌ RuntimeError: No active request in context
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
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):
async def __call__(self, scope, receive, send):
# ... lifespan / non-HTTP handling omitted ...
# Non-Dash routes pass through WITHOUT setting request context
path = scope["path"]
prefix = self.dash_app.config.routes_pathname_prefix
dash_prefix = prefix.rstrip("/") + "/_dash-"
if (
not path.startswith(dash_prefix)
and path != prefix
and path != prefix.rstrip("/")
):
await self.app(scope, receive, send) # ← no context set!
return
# Only Dash-specific routes get context
request = 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()
async def catchall(_request: Request):
return Response(content=dash_app.index(), media_type="text/html")
# ^^^^^^^^^^^^^^^^^
# dash_app.index() internally calls get_current_request()
# which requires the context to be set by DashMiddleware
self.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:
async def __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 stream
path = scope["path"]
prefix = self.dash_app.config.routes_pathname_prefix
dash_prefix = prefix.rstrip("/") + "/_dash-"
if (
not path.startswith(dash_prefix)
and path != prefix
and path != prefix.rstrip("/")
):
await self.app(scope, receive, send)
reset_current_request(token)
return
# Dash route handling with timing, hooks, error handling
try:
await self._setup_timing(request)
await self._run_before_hooks()
await self.app(scope, receive, send)
await self._run_after_hooks()
self._finalize_timing(request)
except Exception as e:
await self._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 scope
set_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:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from dash import Dash, html
from starlette.testclient import TestClient
fastapi_app = FastAPI()
dash_app = Dash(__name__, server=fastapi_app)
dash_app.layout = html.Div("Dash is running")
@fastapi_app.get("/api/echo")
async def echo_get():
return JSONResponse({"method": "GET", "ok": True})
@fastapi_app.post("/api/echo")
async def echo_post(request: Request):
body = await request.json() # ← works correctly, body not consumed by middleware
return JSONResponse({"echo": body})
client = TestClient(fastapi_app)
assert client.post("/api/echo", json={"msg": "hello"}).json() == {"echo": {"msg": "hello"}}
assert client.get("/api/echo").json() == {"method": "GET", "ok": True}
assert client.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: