diff --git a/fastapi/routing.py b/fastapi/routing.py index b8598d7a8..79385eaef 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -360,8 +360,11 @@ def get_request_handler( if dependant.call else EndpointContext() ) + if dependant.path: - endpoint_ctx["path"] = f"{request.method} {dependant.path}" + # For mounted sub-apps, include the mount path prefix + mount_path = request.scope.get("root_path", "").rstrip("/") + endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}" # Read body and auto-close files try: @@ -491,8 +494,9 @@ def get_websocket_app( else EndpointContext() ) if dependant.path: - endpoint_ctx["path"] = f"WS {dependant.path}" - + # For mounted sub-apps, include the mount path prefix + mount_path = websocket.scope.get("root_path", "").rstrip("/") + endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}" async_exit_stack = websocket.scope.get("fastapi_inner_astack") assert isinstance(async_exit_stack, AsyncExitStack), ( "fastapi_inner_astack not found in request scope" diff --git a/tests/test_validation_error_context.py b/tests/test_validation_error_context.py index 90c4367cd..9ea395c08 100644 --- a/tests/test_validation_error_context.py +++ b/tests/test_validation_error_context.py @@ -23,16 +23,28 @@ class ExceptionCapture: app = FastAPI() +sub_app = FastAPI() captured_exception = ExceptionCapture() +app.mount(path="/sub", app=sub_app) + @app.exception_handler(RequestValidationError) +@sub_app.exception_handler(RequestValidationError) async def request_validation_handler(request: Request, exc: RequestValidationError): captured_exception.capture(exc) raise exc +@app.exception_handler(ResponseValidationError) +@sub_app.exception_handler(ResponseValidationError) +async def response_validation_handler(_: Request, exc: ResponseValidationError): + captured_exception.capture(exc) + raise exc + + @app.exception_handler(WebSocketRequestValidationError) +@sub_app.exception_handler(WebSocketRequestValidationError) async def websocket_validation_handler( websocket: WebSocket, exc: WebSocketRequestValidationError ): @@ -50,6 +62,11 @@ def get_item(): return {"name": "Widget"} +@sub_app.get("/items/", response_model=Item) +def get_sub_item(): + return {"name": "Widget"} # pragma: no cover + + @app.websocket("/ws/{item_id}") async def websocket_endpoint(websocket: WebSocket, item_id: int): await websocket.accept() # pragma: no cover @@ -57,6 +74,13 @@ async def websocket_endpoint(websocket: WebSocket, item_id: int): await websocket.close() # pragma: no cover +@sub_app.websocket("/ws/{item_id}") +async def subapp_websocket_endpoint(websocket: WebSocket, item_id: int): + await websocket.accept() # pragma: no cover + await websocket.send_text(f"Item: {item_id}") # pragma: no cover + await websocket.close() # pragma: no cover + + client = TestClient(app) @@ -95,6 +119,33 @@ def test_websocket_validation_error_includes_endpoint_context(): assert "/ws/" in error_str +def test_subapp_request_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + client.get("/sub/items/") + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "get_sub_item" in error_str + assert "/sub/items/" in error_str + + +def test_subapp_websocket_validation_error_includes_endpoint_context(): + captured_exception.exception = None + try: + with client.websocket_connect("/sub/ws/invalid"): + pass # pragma: no cover + except Exception: + pass + + assert captured_exception.exception is not None + error_str = str(captured_exception.exception) + assert "subapp_websocket_endpoint" in error_str + assert "/sub/ws/" in error_str + + def test_validation_error_with_only_path(): errors = [{"type": "missing", "loc": ("body", "name"), "msg": "Field required"}] exc = RequestValidationError(errors, endpoint_ctx={"path": "GET /api/test"})