mirror of
https://github.com/rjNemo/fastapi
synced 2026-06-12 05:26:45 +00:00
✨ add body to RequestValidationError for easier debugging (#853)
This commit is contained in:
parent
180b842a1e
commit
5db99a27cf
8 changed files with 242 additions and 61 deletions
|
|
@ -1,28 +1,27 @@
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI
|
||||||
from fastapi.exception_handlers import (
|
from fastapi.encoders import jsonable_encoder
|
||||||
http_exception_handler,
|
|
||||||
request_validation_exception_handler,
|
|
||||||
)
|
|
||||||
from fastapi.exceptions import RequestValidationError
|
from fastapi.exceptions import RequestValidationError
|
||||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
from pydantic import BaseModel
|
||||||
|
from starlette import status
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(StarletteHTTPException)
|
|
||||||
async def custom_http_exception_handler(request, exc):
|
|
||||||
print(f"OMG! An HTTP error!: {exc}")
|
|
||||||
return await http_exception_handler(request, exc)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(RequestValidationError)
|
@app.exception_handler(RequestValidationError)
|
||||||
async def validation_exception_handler(request, exc):
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
print(f"OMG! The client sent invalid data!: {exc}")
|
return JSONResponse(
|
||||||
return await request_validation_exception_handler(request, exc)
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/items/{item_id}")
|
class Item(BaseModel):
|
||||||
async def read_item(item_id: int):
|
title: str
|
||||||
if item_id == 3:
|
size: int
|
||||||
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
|
|
||||||
return {"item_id": item_id}
|
|
||||||
|
@app.post("/items/")
|
||||||
|
async def create_item(item: Item):
|
||||||
|
return item
|
||||||
|
|
|
||||||
28
docs/src/handling_errors/tutorial006.py
Normal file
28
docs/src/handling_errors/tutorial006.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.exception_handlers import (
|
||||||
|
http_exception_handler,
|
||||||
|
request_validation_exception_handler,
|
||||||
|
)
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(StarletteHTTPException)
|
||||||
|
async def custom_http_exception_handler(request, exc):
|
||||||
|
print(f"OMG! An HTTP error!: {exc}")
|
||||||
|
return await http_exception_handler(request, exc)
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception_handler(RequestValidationError)
|
||||||
|
async def validation_exception_handler(request, exc):
|
||||||
|
print(f"OMG! The client sent invalid data!: {exc}")
|
||||||
|
return await request_validation_exception_handler(request, exc)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/{item_id}")
|
||||||
|
async def read_item(item_id: int):
|
||||||
|
if item_id == 3:
|
||||||
|
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
|
||||||
|
return {"item_id": item_id}
|
||||||
|
|
@ -16,7 +16,6 @@ Some use cases include:
|
||||||
* Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
|
* Converting non-JSON request bodies to JSON (e.g. [`msgpack`](https://msgpack.org/index.html)).
|
||||||
* Decompressing gzip-compressed request bodies.
|
* Decompressing gzip-compressed request bodies.
|
||||||
* Automatically logging all request bodies.
|
* Automatically logging all request bodies.
|
||||||
* Accessing the request body in an exception handler.
|
|
||||||
|
|
||||||
## Handling custom request body encodings
|
## Handling custom request body encodings
|
||||||
|
|
||||||
|
|
@ -71,6 +70,11 @@ But because of our changes in `GzipRequest.body`, the request body will be autom
|
||||||
|
|
||||||
## Accessing the request body in an exception handler
|
## Accessing the request body in an exception handler
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
To solve this same problem, it's probably a lot easier to [use the `body` in a custom handler for `RequestValidationError`](https://fastapi.tiangolo.com/tutorial/handling-errors/#use-the-requestvalidationerror-body).
|
||||||
|
|
||||||
|
But this example is still valid and it shows how to interact with the internal components.
|
||||||
|
|
||||||
We can also use this same approach to access the request body in an exception handler.
|
We can also use this same approach to access the request body in an exception handler.
|
||||||
|
|
||||||
All we need to do is handle the request inside a `try`/`except` block:
|
All we need to do is handle the request inside a `try`/`except` block:
|
||||||
|
|
@ -89,12 +93,12 @@ If an exception occurs, the`Request` instance will still be in scope, so we can
|
||||||
|
|
||||||
You can also set the `route_class` parameter of an `APIRouter`:
|
You can also set the `route_class` parameter of an `APIRouter`:
|
||||||
|
|
||||||
```Python hl_lines="25"
|
```Python hl_lines="28"
|
||||||
{!./src/custom_request_and_route/tutorial003.py!}
|
{!./src/custom_request_and_route/tutorial003.py!}
|
||||||
```
|
```
|
||||||
|
|
||||||
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
|
In this example, the *path operations* under the `router` will use the custom `TimedRoute` class, and will have an extra `X-Response-Time` header in the response with the time it took to generate the response:
|
||||||
|
|
||||||
```Python hl_lines="15 16 17 18 19"
|
```Python hl_lines="15 16 17 18 19 20 21 22"
|
||||||
{!./src/custom_request_and_route/tutorial003.py!}
|
{!./src/custom_request_and_route/tutorial003.py!}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,47 @@ For example, you could want to return a plain text response instead of JSON for
|
||||||
{!./src/handling_errors/tutorial004.py!}
|
{!./src/handling_errors/tutorial004.py!}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use the `RequestValidationError` body
|
||||||
|
|
||||||
|
The `RequestValidationError` contains the `body` it received with invalid data.
|
||||||
|
|
||||||
|
You could use it while developing your app to log the body and debug it, return it to the user, etc.
|
||||||
|
|
||||||
|
```Python hl_lines="16"
|
||||||
|
{!./src/handling_errors/tutorial005.py!}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now try sending an invalid item like:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"title": "towel",
|
||||||
|
"size": "XL"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You will receive a response telling you that the data is invalid containing the received body:
|
||||||
|
|
||||||
|
```JSON hl_lines="13 14 15 16"
|
||||||
|
{
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"loc": [
|
||||||
|
"body",
|
||||||
|
"item",
|
||||||
|
"size"
|
||||||
|
],
|
||||||
|
"msg": "value is not a valid integer",
|
||||||
|
"type": "type_error.integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"title": "towel",
|
||||||
|
"size": "XL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
|
#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
|
||||||
|
|
||||||
**FastAPI** has its own `HTTPException`.
|
**FastAPI** has its own `HTTPException`.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ WebSocketErrorModel = create_model("WebSocket")
|
||||||
|
|
||||||
|
|
||||||
class RequestValidationError(ValidationError):
|
class RequestValidationError(ValidationError):
|
||||||
def __init__(self, errors: Sequence[ErrorList]) -> None:
|
def __init__(self, errors: Sequence[ErrorList], *, body: Any = None) -> None:
|
||||||
|
self.body = body
|
||||||
if PYDANTIC_1:
|
if PYDANTIC_1:
|
||||||
super().__init__(errors, RequestErrorModel)
|
super().__init__(errors, RequestErrorModel)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ def get_request_handler(
|
||||||
)
|
)
|
||||||
values, errors, background_tasks, sub_response, _ = solved_result
|
values, errors, background_tasks, sub_response, _ = solved_result
|
||||||
if errors:
|
if errors:
|
||||||
raise RequestValidationError(errors)
|
raise RequestValidationError(errors, body=body)
|
||||||
else:
|
else:
|
||||||
assert dependant.call is not None, "dependant.call must be a function"
|
assert dependant.call is not None, "dependant.call must be a function"
|
||||||
if is_coroutine:
|
if is_coroutine:
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,18 @@ openapi_schema = {
|
||||||
"openapi": "3.0.2",
|
"openapi": "3.0.2",
|
||||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/items/{item_id}": {
|
"/items/": {
|
||||||
"get": {
|
"post": {
|
||||||
|
"summary": "Create Item",
|
||||||
|
"operationId": "create_item_items__post",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {"$ref": "#/components/schemas/Item"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": True,
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful Response",
|
"description": "Successful Response",
|
||||||
|
|
@ -26,21 +36,31 @@ openapi_schema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"summary": "Read Item",
|
|
||||||
"operationId": "read_item_items__item_id__get",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"required": True,
|
|
||||||
"schema": {"title": "Item Id", "type": "integer"},
|
|
||||||
"name": "item_id",
|
|
||||||
"in": "path",
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Item": {
|
||||||
|
"title": "Item",
|
||||||
|
"required": ["title", "size"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {"title": "Title", "type": "string"},
|
||||||
|
"size": {"title": "Size", "type": "integer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
"ValidationError": {
|
"ValidationError": {
|
||||||
"title": "ValidationError",
|
"title": "ValidationError",
|
||||||
"required": ["loc", "msg", "type"],
|
"required": ["loc", "msg", "type"],
|
||||||
|
|
@ -55,17 +75,6 @@ openapi_schema = {
|
||||||
"type": {"title": "Error Type", "type": "string"},
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"HTTPValidationError": {
|
|
||||||
"title": "HTTPValidationError",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"detail": {
|
|
||||||
"title": "Detail",
|
|
||||||
"type": "array",
|
|
||||||
"items": {"$ref": "#/components/schemas/ValidationError"},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -77,27 +86,23 @@ def test_openapi_schema():
|
||||||
assert response.json() == openapi_schema
|
assert response.json() == openapi_schema
|
||||||
|
|
||||||
|
|
||||||
def test_get_validation_error():
|
def test_post_validation_error():
|
||||||
response = client.get("/items/foo")
|
response = client.post("/items/", json={"title": "towel", "size": "XL"})
|
||||||
assert response.status_code == 422
|
assert response.status_code == 422
|
||||||
assert response.json() == {
|
assert response.json() == {
|
||||||
"detail": [
|
"detail": [
|
||||||
{
|
{
|
||||||
"loc": ["path", "item_id"],
|
"loc": ["body", "item", "size"],
|
||||||
"msg": "value is not a valid integer",
|
"msg": "value is not a valid integer",
|
||||||
"type": "type_error.integer",
|
"type": "type_error.integer",
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"body": {"title": "towel", "size": "XL"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_http_error():
|
def test_post():
|
||||||
response = client.get("/items/3")
|
data = {"title": "towel", "size": 5}
|
||||||
assert response.status_code == 418
|
response = client.post("/items/", json=data)
|
||||||
assert response.json() == {"detail": "Nope! I don't like 3."}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get():
|
|
||||||
response = client.get("/items/2")
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"item_id": 2}
|
assert response.json() == data
|
||||||
|
|
|
||||||
103
tests/test_tutorial/test_handling_errors/test_tutorial006.py
Normal file
103
tests/test_tutorial/test_handling_errors/test_tutorial006.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
from handling_errors.tutorial006 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/items/{item_id}": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"summary": "Read Item",
|
||||||
|
"operationId": "read_item_items__item_id__get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"required": True,
|
||||||
|
"schema": {"title": "Item Id", "type": "integer"},
|
||||||
|
"name": "item_id",
|
||||||
|
"in": "path",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"ValidationError": {
|
||||||
|
"title": "ValidationError",
|
||||||
|
"required": ["loc", "msg", "type"],
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loc": {
|
||||||
|
"title": "Location",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
"msg": {"title": "Message", "type": "string"},
|
||||||
|
"type": {"title": "Error Type", "type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"HTTPValidationError": {
|
||||||
|
"title": "HTTPValidationError",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"detail": {
|
||||||
|
"title": "Detail",
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/ValidationError"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_schema():
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == openapi_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_validation_error():
|
||||||
|
response = client.get("/items/foo")
|
||||||
|
assert response.status_code == 422
|
||||||
|
assert response.json() == {
|
||||||
|
"detail": [
|
||||||
|
{
|
||||||
|
"loc": ["path", "item_id"],
|
||||||
|
"msg": "value is not a valid integer",
|
||||||
|
"type": "type_error.integer",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_http_error():
|
||||||
|
response = client.get("/items/3")
|
||||||
|
assert response.status_code == 418
|
||||||
|
assert response.json() == {"detail": "Nope! I don't like 3."}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get():
|
||||||
|
response = client.get("/items/2")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"item_id": 2}
|
||||||
Loading…
Reference in a new issue