-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: Request, exc: RequestValidationError):
+ return JSONResponse(
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
+ )
-@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)
+class Item(BaseModel):
+ title: str
+ size: int
-@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}
+@app.post("/items/")
+async def create_item(item: Item):
+ return item
--- /dev/null
+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}
* 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
## 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:
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!}
```
{!./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`.
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:
)
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:
"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",
},
},
},
- "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"],
"type": {"title": "Error Type", "type": "string"},
},
},
- "HTTPValidationError": {
- "title": "HTTPValidationError",
- "type": "object",
- "properties": {
- "detail": {
- "title": "Detail",
- "type": "array",
- "items": {"$ref": "#/components/schemas/ValidationError"},
- }
- },
- },
}
},
}
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
--- /dev/null
+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}