From 24a7b664aac825c73c08910e156ec343af55cc53 Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 2 Jun 2026 13:46:12 -0400 Subject: [PATCH 1/3] fix fastapi middleware swallowing user routes bodies --- dash/backends/_fastapi.py | 9 +++- .../backend_tests/test_preconfig_backends.py | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/dash/backends/_fastapi.py b/dash/backends/_fastapi.py index 027b94bed4..594ebfa225 100644 --- a/dash/backends/_fastapi.py +++ b/dash/backends/_fastapi.py @@ -223,7 +223,14 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await self.app(scope, receive, send) return - # HTTP/WebSocket request handling + # Non-Dash routes pass through to avoid consuming body stream + path = scope["path"] + prefix = self.dash_app.config.routes_pathname_prefix + if "_dash-" not in path and path != prefix and path != prefix.rstrip("/"): + await self.app(scope, receive, send) + return + + # HTTP request handling request = Request(scope, receive=receive) token = set_current_request(request) diff --git a/tests/backend_tests/test_preconfig_backends.py b/tests/backend_tests/test_preconfig_backends.py index eec832070a..5f912415ab 100644 --- a/tests/backend_tests/test_preconfig_backends.py +++ b/tests/backend_tests/test_preconfig_backends.py @@ -291,3 +291,46 @@ def test_silence_routes_logging(backend, expected_loggers): assert ( logger.level == logging.ERROR ), f"Logger {logger_name} should be set to ERROR level for {backend} backend" + + +def test_fastapi_custom_post_route(dash_duo): + """Test that user-defined POST routes work with FastAPI backend. + + Regression test for https://github.com/plotly/dash/issues/3801 + The DashMiddleware was consuming the request body for all routes, + causing POST requests to user-defined routes to hang. + """ + from fastapi import FastAPI, Request + from fastapi.responses import JSONResponse + import requests + + fastapi_app = FastAPI() + + @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() + return JSONResponse({"echo": body}) + + app = Dash(__name__, server=fastapi_app) + app.layout = html.Div("Dash is running") + + dash_duo.start_server(app) + + # Test GET request + url = dash_duo.server_url + resp = requests.get(f"{url}/api/echo", timeout=5) + assert resp.status_code == 200 + assert resp.json() == {"method": "GET", "ok": True} + + # Test POST request - this was hanging before the fix + resp = requests.post( + f"{url}/api/echo", + json={"hello": "world"}, + timeout=5, + ) + assert resp.status_code == 200 + assert resp.json() == {"echo": {"hello": "world"}} From 1297eb1fe33dca6202c961e8e19f1a67696ea20d Mon Sep 17 00:00:00 2001 From: philippe Date: Tue, 2 Jun 2026 16:02:20 -0400 Subject: [PATCH 2/3] update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32dc3f3136..1e0404e123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [UNRELEASED] +### Fixed +- [#3805](https://github.com/plotly/dash/pull/3805) Fix FastAPI POST routes deadlock caused by middleware consuming request body. Fixes [#3801](https://github.com/plotly/dash/issues/3801). + ## [4.2.0] - 2026-06-01 - *The Freedom Update* This release marks a major milestone for Dash, bringing unprecedented flexibility to how you build and deploy your applications. From 6b2de3f493ea5107514803cac096f8ffabfc7409 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 3 Jun 2026 17:51:03 -0400 Subject: [PATCH 3/3] improved path check --- dash/backends/_fastapi.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dash/backends/_fastapi.py b/dash/backends/_fastapi.py index 594ebfa225..0516b5edcb 100644 --- a/dash/backends/_fastapi.py +++ b/dash/backends/_fastapi.py @@ -226,7 +226,12 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # Non-Dash routes pass through to avoid consuming body stream path = scope["path"] prefix = self.dash_app.config.routes_pathname_prefix - if "_dash-" not in path and path != prefix and path != prefix.rstrip("/"): + dash_prefix = prefix.rstrip("/") + "/_dash-" + if ( + not path.startswith(dash_prefix) + and path != prefix + and path != prefix.rstrip("/") + ): await self.app(scope, receive, send) return