add body to RequestValidationError for easier debugging (#853)

This commit is contained in:
Aviram Hassan 2020-01-17 13:37:44 +02:00 committed by Sebastián Ramírez
parent 180b842a1e
commit 5db99a27cf
8 changed files with 242 additions and 61 deletions

View File

@ -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

View 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}

View File

@ -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!}
```

View File

@ -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`.

View File

@ -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:

View File

@ -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:

View File

@ -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

View 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}