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. diff --git a/dash/backends/_fastapi.py b/dash/backends/_fastapi.py index 027b94bed4..0516b5edcb 100644 --- a/dash/backends/_fastapi.py +++ b/dash/backends/_fastapi.py @@ -223,7 +223,19 @@ 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 + 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 + + # 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"}}