from fastapi import FastAPI
-from starlette.exceptions import HTTPException
-from starlette.responses import PlainTextResponse
+from starlette.requests import Request
+from starlette.responses import JSONResponse
+
+
+class UnicornException(Exception):
+ def __init__(self, name: str):
+ self.name = name
+
app = FastAPI()
-@app.exception_handler(HTTPException)
-async def http_exception(request, exc):
- return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
+@app.exception_handler(UnicornException)
+async def unicorn_exception_handler(request: Request, exc: UnicornException):
+ return JSONResponse(
+ status_code=418,
+ content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
+ )
-@app.get("/")
-async def root():
- return {"message": "Hello World"}
+@app.get("/unicorns/{name}")
+async def read_unicorn(name: str):
+ if name == "yolo":
+ raise UnicornException(name=name)
+ return {"unicorn_name": name}
--- /dev/null
+from fastapi import FastAPI, HTTPException
+from fastapi.exceptions import RequestValidationError
+from starlette.exceptions import HTTPException as StarletteHTTPException
+from starlette.responses import PlainTextResponse
+
+app = FastAPI()
+
+
+@app.exception_handler(StarletteHTTPException)
+async def http_exception_handler(request, exc):
+ return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(request, exc):
+ return PlainTextResponse(str(exc), status_code=400)
+
+
+@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}
--- /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}
They are handled automatically by **FastAPI** and converted to JSON.
-### Adding custom headers
+## Add custom headers
There are some situations in where it's useful to be able to add custom headers to the HTTP error. For example, for some types of security.
But in case you needed it for an advanced scenario, you can add custom headers:
-
```Python hl_lines="14"
{!./src/handling_errors/tutorial002.py!}
```
-### Installing custom handlers
+## Install custom exception handlers
+
+You can add custom exception handlers with <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
-If you need to add other custom exception handlers, or override the default one (that sends the errors as JSON), you can use <a href="https://www.starlette.io/exceptions/" target="_blank">the same exception utilities from Starlette</a>.
+Let's say you have a custom exception `UnicornException` that you (or a library you use) might `raise`.
-For example, you could override the default exception handler with:
+And you want to handle this exception globally with FastAPI.
-```Python hl_lines="2 3 8 9 10"
+You could add a custom exception handler with `@app.exception_handler()`:
+
+```Python hl_lines="6 7 8 14 15 16 17 18 24"
{!./src/handling_errors/tutorial003.py!}
```
-...this would make it return "plain text" responses with the errors, instead of JSON responses.
+Here, if you request `/unicorns/yolo`, the *path operation* will `raise` a `UnicornException`.
+
+But it will be handled by the `unicorn_exception_handler`.
+
+So, you will receive a clean error, with an HTTP status code of `418` and a JSON content of:
+
+```JSON
+{"message": "Oops! yolo did something. There goes a rainbow..."}
+```
+
+## Override the default exception handlers
+
+**FastAPI** has some default exception handlers.
+
+These handlers are in charge or returning the default JSON responses when you `raise` an `HTTPException` and when the request has invalid data.
+
+You can override these exception handlers with your own.
+
+### Override request validation exceptions
+
+When a request contains invalid data, **FastAPI** internally raises a `RequestValidationError`.
+
+And it also includes a default exception handler for it.
+
+To override it, import the `RequestValidationError` and use it with `@app.exception_handler(RequestValidationError)` to decorate the exception handler.
+
+The exception handler will receive a `Request` and the exception.
+
+```Python hl_lines="2 14 15 16"
+{!./src/handling_errors/tutorial004.py!}
+```
+
+Now, if you go to `/items/foo`, instead of getting the default JSON error with:
+
+```JSON
+{
+ "detail": [
+ {
+ "loc": [
+ "path",
+ "item_id"
+ ],
+ "msg": "value is not a valid integer",
+ "type": "type_error.integer"
+ }
+ ]
+}
+```
+
+you will get a text version, with:
+
+```
+1 validation error
+path -> item_id
+ value is not a valid integer (type=type_error.integer)
+```
+
+#### `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 <a href="https://pydantic-docs.helpmanual.io/#error-handling" target="_blank">`ValidationError`</a>.
+
+**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 a 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
+
+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:
+
+```Python hl_lines="1 3 9 10 11 22"
+{!./src/handling_errors/tutorial004.py!}
+```
+
+#### FastAPI's `HTTPException` vs Starlette's `HTTPException`
+
+**FastAPI** has its own `HTTPException`.
+
+And **FastAPI**'s `HTTPException` error class inherits from Starlette's `HTTPException` error class.
+
+The only difference, is that **FastAPI**'s `HTTPException` allows you to add headers to be included in the response.
+
+This is needed/used internally for OAuth 2.0 and some security utilities.
+
+So, you can keep raising **FastAPI**'s `HTTPException` as normally in your code.
+
+But when you register an exception handler, you should register it for Starlette's `HTTPException`.
+
+This way, if any part of Starlette's internal code, or a Starlette extension or plug-in, raises an `HTTPException`, your handler will be able to catch handle it.
+
+In this example, to be able to have both `HTTPException`s in the same code, Starlette's exceptions is renamed to `StarletteHTTPException`:
+
+```Python
+from starlette.exceptions import HTTPException as StarletteHTTPException
+```
+
+### Re-use **FastAPI**'s exception handlers
+
+You could also just want to use the exception somehow, but then use the same default exception handlers from **FastAPI**.
+
+You can import and re-use the default exception handlers from `fastapi.exception_handlers`:
+
+```Python hl_lines="2 3 4 5 15 21"
+{!./src/handling_errors/tutorial005.py!}
+```
-!!! info
- Note that in this example we set the exception handler with Starlette's `HTTPException` instead of FastAPI's `HTTPException`.
+In this example, you are just `print`ing the error with a very expressive notification.
- This would ensure that if you use a plug-in or any other third-party tool that raises Starlette's `HTTPException` directly, it will be caught by your exception handler.
+But you get the idea, you can use the exception and then just re-use the default exception handlers.
from typing import Any, Callable, Dict, List, Optional, Set, Type, Union
from fastapi import routing
+from fastapi.exception_handlers import (
+ http_exception_handler,
+ request_validation_exception_handler,
+)
+from fastapi.exceptions import RequestValidationError
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
)
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
-from pydantic import BaseModel
from starlette.applications import Starlette
from starlette.exceptions import ExceptionMiddleware, HTTPException
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.routing import BaseRoute
-async def http_exception(request: Request, exc: HTTPException) -> JSONResponse:
- headers = getattr(exc, "headers", None)
- if headers:
- return JSONResponse(
- {"detail": exc.detail}, status_code=exc.status_code, headers=headers
- )
- else:
- return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
-
-
class FastAPI(Starlette):
def __init__(
self,
)
self.add_route(self.redoc_url, redoc_html, include_in_schema=False)
- self.add_exception_handler(HTTPException, http_exception)
+ self.add_exception_handler(HTTPException, http_exception_handler)
+ self.add_exception_handler(
+ RequestValidationError, request_validation_exception_handler
+ )
def add_api_route(
self,
--- /dev/null
+from fastapi.exceptions import RequestValidationError
+from starlette.exceptions import HTTPException
+from starlette.requests import Request
+from starlette.responses import JSONResponse
+from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
+
+
+async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
+ headers = getattr(exc, "headers", None)
+ if headers:
+ return JSONResponse(
+ {"detail": exc.detail}, status_code=exc.status_code, headers=headers
+ )
+ else:
+ return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
+
+
+async def request_validation_exception_handler(
+ request: Request, exc: RequestValidationError
+) -> JSONResponse:
+ return JSONResponse(
+ status_code=HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": exc.errors()}
+ )
+from pydantic import ValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
) -> None:
super().__init__(status_code=status_code, detail=detail)
self.headers = headers
+
+
+class RequestValidationError(ValidationError):
+ pass
+
+
+class WebSocketRequestValidationError(ValidationError):
+ pass
solve_dependencies,
)
from fastapi.encoders import jsonable_encoder
+from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
from pydantic import BaseConfig, BaseModel, Schema
from pydantic.error_wrappers import ErrorWrapper, ValidationError
from pydantic.fields import Field
request_response,
websocket_session,
)
-from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, WS_1008_POLICY_VIOLATION
+from starlette.status import WS_1008_POLICY_VIOLATION
from starlette.websockets import WebSocket
request=request, dependant=dependant, body=body
)
if errors:
- errors_out = ValidationError(errors)
- raise HTTPException(
- status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
- )
+ raise RequestValidationError(errors)
else:
assert dependant.call is not None, "dependant.call must be a function"
if is_coroutine:
)
if errors:
await websocket.close(code=WS_1008_POLICY_VIOLATION)
- errors_out = ValidationError(errors)
- raise HTTPException(
- status_code=HTTP_422_UNPROCESSABLE_ENTITY, detail=errors_out.errors()
- )
+ raise WebSocketRequestValidationError(errors)
assert dependant.call is not None, "dependant.call must me a function"
await dependant.call(**values)
--- /dev/null
+from starlette.testclient import TestClient
+
+from handling_errors.tutorial003 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "Fast API", "version": "0.1.0"},
+ "paths": {
+ "/unicorns/{name}": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "summary": "Read Unicorn",
+ "operationId": "read_unicorn_unicorns__name__get",
+ "parameters": [
+ {
+ "required": True,
+ "schema": {"title": "Name", "type": "string"},
+ "name": "name",
+ "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():
+ response = client.get("/unicorns/shinny")
+ assert response.status_code == 200
+ assert response.json() == {"unicorn_name": "shinny"}
+
+
+def test_get_exception():
+ response = client.get("/unicorns/yolo")
+ assert response.status_code == 418
+ assert response.json() == {
+ "message": "Oops! yolo did something. There goes a rainbow..."
+ }
--- /dev/null
+from starlette.testclient import TestClient
+
+from handling_errors.tutorial004 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 == 400
+ validation_error_str_lines = [
+ b"1 validation error",
+ b"path -> item_id",
+ b" value is not a valid integer (type=type_error.integer)",
+ ]
+ assert response.content == b"\n".join(validation_error_str_lines)
+
+
+def test_get_http_error():
+ response = client.get("/items/3")
+ assert response.status_code == 418
+ assert response.content == b"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}
--- /dev/null
+from starlette.testclient import TestClient
+
+from handling_errors.tutorial005 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}