From bf8f4068b76243a32330fdaa9625bf506922a7f9 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Thu, 27 Nov 2025 17:09:36 +0100 Subject: [PATCH] Add variants for `custom_request_and_route/tutorial002` --- .../docs/how-to/custom-request-and-route.md | 4 +-- .../tutorial002_an.py | 29 +++++++++++++++++++ .../tutorial002_an_py310.py | 29 +++++++++++++++++++ .../tutorial002_an_py39.py | 29 +++++++++++++++++++ .../tutorial002_py310.py | 29 +++++++++++++++++++ .../tutorial002_py39.py | 29 +++++++++++++++++++ pyproject.toml | 5 ++++ .../test_tutorial002.py | 29 +++++++++++++++---- 8 files changed, 176 insertions(+), 7 deletions(-) create mode 100644 docs_src/custom_request_and_route/tutorial002_an.py create mode 100644 docs_src/custom_request_and_route/tutorial002_an_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial002_an_py39.py create mode 100644 docs_src/custom_request_and_route/tutorial002_py310.py create mode 100644 docs_src/custom_request_and_route/tutorial002_py39.py diff --git a/docs/en/docs/how-to/custom-request-and-route.md b/docs/en/docs/how-to/custom-request-and-route.md index a36f28750..8e1f353cd 100644 --- a/docs/en/docs/how-to/custom-request-and-route.md +++ b/docs/en/docs/how-to/custom-request-and-route.md @@ -92,11 +92,11 @@ We can also use this same approach to access the request body in an exception ha All we need to do is handle the request inside a `try`/`except` block: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[13,15] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[13,15] *} If an exception occurs, the`Request` instance will still be in scope, so we can read and make use of the request body when handling the error: -{* ../../docs_src/custom_request_and_route/tutorial002.py hl[16:18] *} +{* ../../docs_src/custom_request_and_route/tutorial002_an_py310.py hl[16:18] *} ## Custom `APIRoute` class in a router { #custom-apiroute-class-in-a-router } diff --git a/docs_src/custom_request_and_route/tutorial002_an.py b/docs_src/custom_request_and_route/tutorial002_an.py new file mode 100644 index 000000000..cee4a95f0 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an.py @@ -0,0 +1,29 @@ +from typing import Callable, List + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: List[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_an_py310.py b/docs_src/custom_request_and_route/tutorial002_an_py310.py new file mode 100644 index 000000000..13a5ca542 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an_py310.py @@ -0,0 +1,29 @@ +from collections.abc import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_an_py39.py b/docs_src/custom_request_and_route/tutorial002_an_py39.py new file mode 100644 index 000000000..c4e474828 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_an_py39.py @@ -0,0 +1,29 @@ +from typing import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_py310.py b/docs_src/custom_request_and_route/tutorial002_py310.py new file mode 100644 index 000000000..13a5ca542 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_py310.py @@ -0,0 +1,29 @@ +from collections.abc import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/docs_src/custom_request_and_route/tutorial002_py39.py b/docs_src/custom_request_and_route/tutorial002_py39.py new file mode 100644 index 000000000..c4e474828 --- /dev/null +++ b/docs_src/custom_request_and_route/tutorial002_py39.py @@ -0,0 +1,29 @@ +from typing import Callable + +from fastapi import Body, FastAPI, HTTPException, Request, Response +from fastapi.exceptions import RequestValidationError +from fastapi.routing import APIRoute + + +class ValidationErrorLoggingRoute(APIRoute): + def get_route_handler(self) -> Callable: + original_route_handler = super().get_route_handler() + + async def custom_route_handler(request: Request) -> Response: + try: + return await original_route_handler(request) + except RequestValidationError as exc: + body = await request.body() + detail = {"errors": exc.errors(), "body": body.decode()} + raise HTTPException(status_code=422, detail=detail) + + return custom_route_handler + + +app = FastAPI() +app.router.route_class = ValidationErrorLoggingRoute + + +@app.post("/") +async def sum_numbers(numbers: list[int] = Body()): + return sum(numbers) diff --git a/pyproject.toml b/pyproject.toml index f91fea0a4..f8d5fa7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -240,6 +240,11 @@ ignore = [ "docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"] "docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py" = ["B904"] "docs_src/custom_request_and_route/tutorial002.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_py39.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_py310.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_an.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_an_py39.py" = ["B904"] +"docs_src/custom_request_and_route/tutorial002_an_py310.py" = ["B904"] "docs_src/dependencies/tutorial008_an.py" = ["F821"] "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"] "docs_src/query_params_str_validations/tutorial012_an.py" = ["B006"] diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py index 647f1c5dd..c35752ed1 100644 --- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py +++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py @@ -1,17 +1,36 @@ +import importlib + +import pytest from dirty_equals import IsDict, IsOneOf from fastapi.testclient import TestClient -from docs_src.custom_request_and_route.tutorial002 import app - -client = TestClient(app) +from tests.utils import needs_py39, needs_py310 -def test_endpoint_works(): +@pytest.fixture( + name="client", + params=[ + pytest.param("tutorial002"), + pytest.param("tutorial002_py39", marks=needs_py39), + pytest.param("tutorial002_py310", marks=needs_py310), + pytest.param("tutorial002_an"), + pytest.param("tutorial002_an_py39", marks=needs_py39), + pytest.param("tutorial002_an_py310", marks=needs_py310), + ], +) +def get_client(request: pytest.FixtureRequest): + mod = importlib.import_module(f"docs_src.custom_request_and_route.{request.param}") + + client = TestClient(mod.app) + return client + + +def test_endpoint_works(client: TestClient): response = client.post("/", json=[1, 2, 3]) assert response.json() == 6 -def test_exception_handler_body_access(): +def test_exception_handler_body_access(client: TestClient): response = client.post("/", json={"numbers": [1, 2, 3]}) assert response.json() == IsDict( {