mirror of https://github.com/tiangolo/fastapi.git
✨ add body to RequestValidationError for easier debugging (#853)
This commit is contained in:
parent
180b842a1e
commit
5db99a27cf
|
|
@ -1,28 +1,27 @@
|
|||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exception_handlers import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
)
|
||||
from fastapi import FastAPI
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
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.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)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
||||
)
|
||||
|
||||
|
||||
@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}
|
||||
class Item(BaseModel):
|
||||
title: str
|
||||
size: int
|
||||
|
||||
|
||||
@app.post("/items/")
|
||||
async def create_item(item: Item):
|
||||
return item
|
||||
|
|
|
|||
|
|
@ -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)).
|
||||
* Decompressing gzip-compressed request bodies.
|
||||
* Automatically logging all request bodies.
|
||||
* Accessing the request body in an exception handler.
|
||||
|
||||
## 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
|
||||
|
||||
!!! 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.
|
||||
|
||||
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`:
|
||||
|
||||
```Python hl_lines="25"
|
||||
```Python hl_lines="28"
|
||||
{!./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:
|
||||
|
||||
```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!}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -176,6 +176,47 @@ For example, you could want to return a plain text response instead of JSON for
|
|||
{!./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** has its own `HTTPException`.
|
||||
|
|
|
|||
|
|
@ -21,7 +21,8 @@ WebSocketErrorModel = create_model("WebSocket")
|
|||
|
||||
|
||||
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:
|
||||
super().__init__(errors, RequestErrorModel)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ def get_request_handler(
|
|||
)
|
||||
values, errors, background_tasks, sub_response, _ = solved_result
|
||||
if errors:
|
||||
raise RequestValidationError(errors)
|
||||
raise RequestValidationError(errors, body=body)
|
||||
else:
|
||||
assert dependant.call is not None, "dependant.call must be a function"
|
||||
if is_coroutine:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,18 @@ openapi_schema = {
|
|||
"openapi": "3.0.2",
|
||||
"info": {"title": "Fast API", "version": "0.1.0"},
|
||||
"paths": {
|
||||
"/items/{item_id}": {
|
||||
"get": {
|
||||
"/items/": {
|
||||
"post": {
|
||||
"summary": "Create Item",
|
||||
"operationId": "create_item_items__post",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/Item"}
|
||||
}
|
||||
},
|
||||
"required": True,
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"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": {
|
||||
"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": {
|
||||
"title": "ValidationError",
|
||||
"required": ["loc", "msg", "type"],
|
||||
|
|
@ -55,17 +75,6 @@ openapi_schema = {
|
|||
"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
|
||||
|
||||
|
||||
def test_get_validation_error():
|
||||
response = client.get("/items/foo")
|
||||
def test_post_validation_error():
|
||||
response = client.post("/items/", json={"title": "towel", "size": "XL"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["path", "item_id"],
|
||||
"loc": ["body", "item", "size"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
],
|
||||
"body": {"title": "towel", "size": "XL"},
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
def test_post():
|
||||
data = {"title": "towel", "size": 5}
|
||||
response = client.post("/items/", json=data)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"item_id": 2}
|
||||
assert response.json() == data
|
||||
|
|
|
|||
|
|
@ -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 New Issue