From 111de8995783ca3a8b8bbdbabae6d15bb2009ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Thu, 30 Oct 2025 10:50:23 +0100 Subject: [PATCH 01/19] Add check for ciruclar imports + test --- fastapi/routing.py | 1 + tests/test_router_circular_import.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 tests/test_router_circular_import.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 0b59d250a..eb3d2c620 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1333,6 +1333,7 @@ class APIRouter(routing.Router): app.include_router(internal_router) ``` """ + assert self is not router, "Cannot include router into itself" if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py new file mode 100644 index 000000000..dfec8d24c --- /dev/null +++ b/tests/test_router_circular_import.py @@ -0,0 +1,11 @@ +import pytest +from fastapi import APIRouter, FastAPI + + +def test_router_circular_import(): + app = FastAPI() + router = APIRouter() + + app.include_router(router) + with pytest.raises(AssertionError, match="Router cannot be the same as parent"): + router.include_router(router) \ No newline at end of file From ddfaa1e44172b1cfcf1cadc9e7d6e27b82ec5e01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:51:09 +0000 Subject: [PATCH 02/19] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_router_circular_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py index dfec8d24c..d2ac71b29 100644 --- a/tests/test_router_circular_import.py +++ b/tests/test_router_circular_import.py @@ -8,4 +8,4 @@ def test_router_circular_import(): app.include_router(router) with pytest.raises(AssertionError, match="Router cannot be the same as parent"): - router.include_router(router) \ No newline at end of file + router.include_router(router) From c0fee03fdfd1a3b025642df8d38a36411d9d0423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Thu, 30 Oct 2025 10:54:18 +0100 Subject: [PATCH 03/19] correct error msg --- tests/test_router_circular_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py index d2ac71b29..f74a49109 100644 --- a/tests/test_router_circular_import.py +++ b/tests/test_router_circular_import.py @@ -7,5 +7,5 @@ def test_router_circular_import(): router = APIRouter() app.include_router(router) - with pytest.raises(AssertionError, match="Router cannot be the same as parent"): + with pytest.raises(AssertionError, match="Cannot include router into itself"): router.include_router(router) From 28fa21fd1a1d08c2e6ee715feb3d979bf2662cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:05:57 +0100 Subject: [PATCH 04/19] Update tests/test_router_circular_import.py Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- tests/test_router_circular_import.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py index f74a49109..a00205b39 100644 --- a/tests/test_router_circular_import.py +++ b/tests/test_router_circular_import.py @@ -1,11 +1,9 @@ import pytest -from fastapi import APIRouter, FastAPI +from fastapi import APIRouter def test_router_circular_import(): - app = FastAPI() router = APIRouter() - app.include_router(router) with pytest.raises(AssertionError, match="Cannot include router into itself"): router.include_router(router) From 4c9cf7439f8e769af600c87190a44367583526cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:06:06 +0100 Subject: [PATCH 05/19] Update fastapi/routing.py Co-authored-by: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> --- fastapi/routing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index eb3d2c620..a6ed2f328 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1333,7 +1333,10 @@ class APIRouter(routing.Router): app.include_router(internal_router) ``` """ - assert self is not router, "Cannot include router into itself" + assert self is not router, ( + "Cannot include the same APIRouter instance into itself. " + "Did you mean to include a different router?" + ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( From 6762879a3a238c882f56b36a60b0f80c1798944c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Thu, 30 Oct 2025 13:10:11 +0100 Subject: [PATCH 06/19] new msg error --- tests/test_router_circular_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py index a00205b39..e4d614d56 100644 --- a/tests/test_router_circular_import.py +++ b/tests/test_router_circular_import.py @@ -5,5 +5,5 @@ from fastapi import APIRouter def test_router_circular_import(): router = APIRouter() - with pytest.raises(AssertionError, match="Cannot include router into itself"): + with pytest.raises(AssertionError, match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?"): router.include_router(router) From 3d4c85c1baeb7b54cae00a667950ac8ed34b09db Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:10:25 +0000 Subject: [PATCH 07/19] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_router_circular_import.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py index e4d614d56..492a26d00 100644 --- a/tests/test_router_circular_import.py +++ b/tests/test_router_circular_import.py @@ -5,5 +5,8 @@ from fastapi import APIRouter def test_router_circular_import(): router = APIRouter() - with pytest.raises(AssertionError, match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?"): + with pytest.raises( + AssertionError, + match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?", + ): router.include_router(router) From a704d7e51439b56f0f3c034248efbec806f8504c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:02:29 +0100 Subject: [PATCH 08/19] Add RequestMalformedError exception class --- fastapi/exceptions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index 0620428be..a614874e8 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -162,6 +162,12 @@ class ValidationException(Exception): return self._errors +class RequestMalformedError(ValidationException): + def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: + super().__init__(errors) + self.body = body + + class RequestValidationError(ValidationException): def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: super().__init__(errors) From 4165d1cf6c71238daa00fb8704c388deecb51e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:03:14 +0100 Subject: [PATCH 09/19] Add handler for RequestMalformedError exceptions Added a new exception handler for RequestMalformedError to return a 400 status code with error details. --- fastapi/exception_handlers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 475dd7bdd..446793848 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,5 +1,5 @@ from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError +from fastapi.exceptions import RequestMalformedError, RequestValidationError, WebSocketRequestValidationError from fastapi.utils import is_body_allowed_for_status_code from fastapi.websockets import WebSocket from starlette.exceptions import HTTPException @@ -17,6 +17,15 @@ async def http_exception_handler(request: Request, exc: HTTPException) -> Respon ) +async def request_malformed_exception_handler( + request: Request, exc: RequestMalformedError +) -> JSONResponse: + return JSONResponse( + status_code=400, + content={"detail": jsonable_encoder(exc.errors())}, + ) + + async def request_validation_exception_handler( request: Request, exc: RequestValidationError ) -> JSONResponse: From a3bd484b8c4aff670756609dfe333fb5446028eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:05:18 +0100 Subject: [PATCH 10/19] Add RequestMalformedError handler to exception handling --- fastapi/applications.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 0a47699ae..9e9460041 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -18,10 +18,15 @@ from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( http_exception_handler, + request_malformed_exception_handler request_validation_exception_handler, websocket_request_validation_exception_handler, ) -from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError +from fastapi.exceptions import ( + RequestMalformedError, + RequestValidationError, + WebSocketRequestValidationError +) from fastapi.logger import logger from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware from fastapi.openapi.docs import ( @@ -979,6 +984,9 @@ class FastAPI(Starlette): Any, Callable[[Request, Any], Union[Response, Awaitable[Response]]] ] = {} if exception_handlers is None else dict(exception_handlers) self.exception_handlers.setdefault(HTTPException, http_exception_handler) + self.exception_handlers.setdefault( + RequestMalformedError, request_malformed_exception_handler + ) self.exception_handlers.setdefault( RequestValidationError, request_validation_exception_handler ) From 161185b41689e2bb13e6b12655aac9bb0d48ff4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:06:19 +0100 Subject: [PATCH 11/19] Change validation error type to RequestMalformedError --- fastapi/routing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 144879ea2..3bd0e33f4 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -48,6 +48,7 @@ from fastapi.dependencies.utils import ( from fastapi.encoders import jsonable_encoder from fastapi.exceptions import ( FastAPIError, + RequestMalformedError, RequestValidationError, ResponseValidationError, WebSocketRequestValidationError, @@ -350,7 +351,7 @@ def get_request_handler( else: body = body_bytes except json.JSONDecodeError as e: - validation_error = RequestValidationError( + validation_error = RequestMalformedError( [ { "type": "json_invalid", From 26278c37b43065c718bdc1e6e1e0efbf20287093 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 00:08:42 +0000 Subject: [PATCH 12/19] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/exception_handlers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fastapi/exception_handlers.py b/fastapi/exception_handlers.py index 446793848..5cc0976af 100644 --- a/fastapi/exception_handlers.py +++ b/fastapi/exception_handlers.py @@ -1,5 +1,9 @@ from fastapi.encoders import jsonable_encoder -from fastapi.exceptions import RequestMalformedError, RequestValidationError, WebSocketRequestValidationError +from fastapi.exceptions import ( + RequestMalformedError, + RequestValidationError, + WebSocketRequestValidationError, +) from fastapi.utils import is_body_allowed_for_status_code from fastapi.websockets import WebSocket from starlette.exceptions import HTTPException From e4871bc4d5fce410d6e3def866426e4e2bca769f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:10:21 +0100 Subject: [PATCH 13/19] Delete tests/test_router_circular_import.py --- tests/test_router_circular_import.py | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 tests/test_router_circular_import.py diff --git a/tests/test_router_circular_import.py b/tests/test_router_circular_import.py deleted file mode 100644 index 492a26d00..000000000 --- a/tests/test_router_circular_import.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest -from fastapi import APIRouter - - -def test_router_circular_import(): - router = APIRouter() - - with pytest.raises( - AssertionError, - match="Cannot include the same APIRouter instance into itself. Did you mean to include a different router?", - ): - router.include_router(router) From fe49c85e9e8833066a911c188e67f916a698ea78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 01:11:00 +0100 Subject: [PATCH 14/19] Remove self-inclusion assertion for APIRouter --- fastapi/routing.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 3bd0e33f4..d7a585df1 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1339,10 +1339,6 @@ class APIRouter(routing.Router): app.include_router(internal_router) ``` """ - assert self is not router, ( - "Cannot include the same APIRouter instance into itself. " - "Did you mean to include a different router?" - ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" assert not prefix.endswith("/"), ( From a244fd8ad2c38b972b1787b693a05013df4dbdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez=20Castro?= <72013291+JavierSanchezCastro@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:15:09 +0100 Subject: [PATCH 15/19] Fix formatting issue in applications.py --- fastapi/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index 9e9460041..fe26a8309 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -18,7 +18,7 @@ from fastapi import routing from fastapi.datastructures import Default, DefaultPlaceholder from fastapi.exception_handlers import ( http_exception_handler, - request_malformed_exception_handler + request_malformed_exception_handler, request_validation_exception_handler, websocket_request_validation_exception_handler, ) From 26ca9444aaf154adeb3f2681650cf0c8be246171 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:15:59 +0000 Subject: [PATCH 16/19] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/applications.py b/fastapi/applications.py index fe26a8309..08fbf4d85 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -25,7 +25,7 @@ from fastapi.exception_handlers import ( from fastapi.exceptions import ( RequestMalformedError, RequestValidationError, - WebSocketRequestValidationError + WebSocketRequestValidationError, ) from fastapi.logger import logger from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware From cf424613b3f52884c607ac64e5f74c94fbc57267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Mon, 24 Nov 2025 12:22:09 +0100 Subject: [PATCH 17/19] update test --- tests/test_tutorial/test_body/test_tutorial001.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py index f8b5aee8d..a595b0ec1 100644 --- a/tests/test_tutorial/test_body/test_tutorial001.py +++ b/tests/test_tutorial/test_body/test_tutorial001.py @@ -200,7 +200,7 @@ def test_post_broken_body(client: TestClient): headers={"content-type": "application/json"}, content="{some broken json}", ) - assert response.status_code == 422, response.text + assert response.status_code == 400, response.text assert response.json() == IsDict( { "detail": [ From 5302f988cf45c066904de5b7d85b6b2043247fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Mon, 24 Nov 2025 12:30:50 +0100 Subject: [PATCH 18/19] linter --- fastapi/routing.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index d7a585df1..6e04629ce 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -351,7 +351,7 @@ def get_request_handler( else: body = body_bytes except json.JSONDecodeError as e: - validation_error = RequestMalformedError( + raise RequestMalformedError( [ { "type": "json_invalid", @@ -362,8 +362,7 @@ def get_request_handler( } ], body=e.doc, - ) - raise validation_error from e + ) from e except HTTPException: # If a middleware raises an HTTPException, it should be raised again raise From 74fc74564b9ba9935c699e430a1f3283658f42b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20S=C3=A1nchez?= Date: Sat, 6 Dec 2025 13:51:03 +0100 Subject: [PATCH 19/19] add new endpoint_ctx --- fastapi/exceptions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/fastapi/exceptions.py b/fastapi/exceptions.py index ac5b0e0a7..33f3cf9c0 100644 --- a/fastapi/exceptions.py +++ b/fastapi/exceptions.py @@ -200,8 +200,14 @@ class ValidationException(Exception): class RequestMalformedError(ValidationException): - def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None: - super().__init__(errors) + def __init__( + self, + errors: Sequence[Any], + *, + body: Any = None, + endpoint_ctx: Optional[EndpointContext] = None, + ) -> None: + super().__init__(errors, endpoint_ctx=endpoint_ctx) self.body = body