Repository navigation
[BUG] RuntimeError: No active request in context on catchall route (FastAPI backend) #3812
Description
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
mainbranch 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 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):
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:
DashMiddlewaresees it's not a Dash path → skips setting request context → passes through- FastAPI router matches the
catchallroute → callsdash_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 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:
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
Activity
rgajason commentedon Jun 24, 2026
While I'm able to get the same / similar stack trace as you, it's not when I'm hitting a non-Dash route - it's when I'm requesting an endpoint that doesn't exist (should be a 404, but instead getting a 500).
Using your example setup as follows:
Setup:
app.pypip install 'dash[fastapi]'uvicorn app:server --host 0.0.0.0 --port 8050If I use
curlto hit the defined endpoints (/api/echo) there are no issues - HTTP 200, content as expected. But if I use a desktop web browser (such as Firefox), I get the 500 error and stack trace on the additional/favicon.icorequest that the desktop browser sends which cURL does not (the/api/echopart of the request still returns 200 to the browser).I can replicate this 500/stack trace by trying to request any non-existent endpoint (
/jfkdlsjfdlsk).Not trying to say there isn't an issue here, just saying that I think it is related to users requesting undefined content in general.