From cd9d093f603b81f34eb2751e9ca4522089fa9696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Wed, 10 Dec 2025 04:56:50 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20about=20re-raisi?= =?UTF-8?q?ng=20validation=20errors,=20do=20not=20include=20string=20as=20?= =?UTF-8?q?is=20to=20not=20leak=20information=20(#14487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/tutorial/handling-errors.md | 35 +++++++------------ docs_src/handling_errors/tutorial004.py | 7 ++-- .../test_handling_errors/test_tutorial004.py | 14 ++------ 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/docs/en/docs/tutorial/handling-errors.md b/docs/en/docs/tutorial/handling-errors.md index 53501837c..4092039b1 100644 --- a/docs/en/docs/tutorial/handling-errors.md +++ b/docs/en/docs/tutorial/handling-errors.md @@ -127,7 +127,7 @@ To override it, import the `RequestValidationError` and use it with `@app.except The exception handler will receive a `Request` and the exception. -{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:16] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[2,14:19] *} Now, if you go to `/items/foo`, instead of getting the default JSON error with: @@ -149,36 +149,17 @@ Now, if you go to `/items/foo`, instead of getting the default JSON error with: you will get a text version, with: ``` -1 validation error -path -> item_id - value is not a valid integer (type=type_error.integer) +Validation errors: +Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer ``` -#### `RequestValidationError` vs `ValidationError` { #requestvalidationerror-vs-validationerror } - -/// warning - -These are technical details that you might skip if it's not important for you now. - -/// - -`RequestValidationError` is a sub-class of Pydantic's `ValidationError`. - -**FastAPI** uses it so that, if you use a Pydantic model in `response_model`, and your data has an error, you will see the error in your log. - -But the client/user will not see it. Instead, the client will receive an "Internal Server Error" with an HTTP status code `500`. - -It should be this way because if you have a Pydantic `ValidationError` in your *response* or anywhere in your code (not in the client's *request*), it's actually a bug in your code. - -And while you fix it, your clients/users shouldn't have access to internal information about the error, as that could expose a security vulnerability. - ### Override the `HTTPException` error handler { #override-the-httpexception-error-handler } The same way, you can override the `HTTPException` handler. For example, you could want to return a plain text response instead of JSON for these errors: -{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,22] *} +{* ../../docs_src/handling_errors/tutorial004.py hl[3:4,9:11,25] *} /// note | Technical Details @@ -188,6 +169,14 @@ You could also use `from starlette.responses import PlainTextResponse`. /// +/// warning + +Have in mind that the `RequestValidationError` contains the information of the file name and line where the validation error happens so that you can show it in your logs with the relevant information if you want to. + +But that means that if you just convert it to a string and return that information directly, you could be leaking a bit of information about your system, that's why here the code extracts and shows each error independently. + +/// + ### Use the `RequestValidationError` body { #use-the-requestvalidationerror-body } The `RequestValidationError` contains the `body` it received with invalid data. diff --git a/docs_src/handling_errors/tutorial004.py b/docs_src/handling_errors/tutorial004.py index 300a3834f..ae50807e9 100644 --- a/docs_src/handling_errors/tutorial004.py +++ b/docs_src/handling_errors/tutorial004.py @@ -12,8 +12,11 @@ async def http_exception_handler(request, exc): @app.exception_handler(RequestValidationError) -async def validation_exception_handler(request, exc): - return PlainTextResponse(str(exc), status_code=400) +async def validation_exception_handler(request, exc: RequestValidationError): + message = "Validation errors:" + for error in exc.errors(): + message += f"\nField: {error['loc']}, Error: {error['msg']}" + return PlainTextResponse(message, status_code=400) @app.get("/items/{item_id}") diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial004.py b/tests/test_tutorial/test_handling_errors/test_tutorial004.py index 217159a59..c04bf3724 100644 --- a/tests/test_tutorial/test_handling_errors/test_tutorial004.py +++ b/tests/test_tutorial/test_handling_errors/test_tutorial004.py @@ -8,18 +8,8 @@ client = TestClient(app) def test_get_validation_error(): response = client.get("/items/foo") assert response.status_code == 400, response.text - # TODO: remove when deprecating Pydantic v1 - assert ( - # TODO: remove when deprecating Pydantic v1 - "path -> item_id" in response.text - or "'loc': ('path', 'item_id')" in response.text - ) - assert ( - # TODO: remove when deprecating Pydantic v1 - "value is not a valid integer" in response.text - or "Input should be a valid integer, unable to parse string as an integer" - in response.text - ) + assert "Validation errors:" in response.text + assert "Field: ('path', 'item_id')" in response.text def test_get_http_error():