122 lines
4.5 KiB
Markdown
122 lines
4.5 KiB
Markdown
# Errors
|
|
|
|
Use handlers and dependencies to signal HTTP meaning clearly, and let FastAPI or Starlette own the wire format.
|
|
|
|
## Why this convention exists
|
|
|
|
FastAPI sits at a boundary where several kinds of failures meet:
|
|
|
|
- invalid requests
|
|
- permission failures
|
|
- missing resources
|
|
- domain or infrastructure errors
|
|
- framework/runtime exceptions
|
|
|
|
Good code keeps those concerns separate.
|
|
|
|
The stable pattern is:
|
|
|
|
- handlers and dependencies raise semantic HTTP exceptions for boundary failures
|
|
- request validation errors come from FastAPI and Pydantic automatically
|
|
- exception handlers turn those failures into HTTP responses
|
|
- tests assert the HTTP contract, not private exception plumbing
|
|
|
|
That keeps route code small and error responses consistent.
|
|
|
|
## The convention
|
|
|
|
1. Raise `HTTPException` for request-level failures such as 400, 401, 403, and 404.
|
|
2. Let FastAPI and Pydantic produce validation errors instead of hand-rolling error dicts.
|
|
3. Centralize error-envelope formatting in exception handlers when the API needs a stable shape.
|
|
4. Keep internal exception details out of client responses unless that disclosure is part of the contract.
|
|
5. Keep domain exceptions framework-agnostic and translate them at the HTTP boundary.
|
|
|
|
## Follow it at the HTTP boundary
|
|
|
|
Use this whenever the failure is about what the caller sent, what the caller can access, or what the API contract should look like on failure.
|
|
|
|
## Avoid these mistakes
|
|
|
|
Do *not* raise `HTTPException` from deep reusable domain code.
|
|
|
|
Do *not* build ad hoc error JSON in each route.
|
|
|
|
Do *not* catch every exception just to expose `str(exc)` to clients.
|
|
|
|
## Preferred shape
|
|
|
|
```python
|
|
@router.get("/{id}")
|
|
def read_item(session: SessionDep, current_user: CurrentUser, id: UUID) -> ItemPublic:
|
|
item = session.get(Item, id)
|
|
if not item:
|
|
raise HTTPException(status_code=404, detail="Item not found")
|
|
if item.owner_id != current_user.id:
|
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
|
return item
|
|
```
|
|
|
|
Why this works:
|
|
|
|
- the failure is explicit at the boundary
|
|
- the status code carries the meaning
|
|
- the framework serializes it consistently
|
|
|
|
## Counterexamples
|
|
|
|
### Returning error dicts from handlers
|
|
|
|
```python
|
|
return {"error": "not_found", "message": "Item not found"}
|
|
```
|
|
|
|
Bad because success and failure flows blur together and status handling becomes inconsistent.
|
|
|
|
### Framework exceptions in core domain code
|
|
|
|
```python
|
|
class BillingService:
|
|
def charge(...):
|
|
raise HTTPException(status_code=402, detail="Payment required")
|
|
```
|
|
|
|
Bad because domain code is now coupled to one web framework.
|
|
|
|
### Catch-all 500 handling that leaks internals
|
|
|
|
```python
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=str(exc))
|
|
```
|
|
|
|
Bad because it leaks internal details and destroys useful operational distinctions.
|
|
|
|
## Source signals
|
|
|
|
### FastAPI
|
|
|
|
- `fastapi/exception_handlers.py:11-26` is the core citation here: `HTTPException` becomes a JSON `{"detail": ...}` response when a body is allowed, and `RequestValidationError` becomes a 422 response using `exc.errors()`.
|
|
- `docs_src/bigger_applications/app_an_py310/routers/items.py:21-25` raises `HTTPException(status_code=404, detail="Item not found")` for a missing resource.
|
|
- `docs_src/bigger_applications/app_an_py310/routers/items.py:33-37` raises `HTTPException(status_code=403, detail=...)` for a forbidden update.
|
|
|
|
### Full-stack FastAPI Template
|
|
|
|
- `backend/app/api/deps.py:30-45` translates invalid token, missing user, and inactive user states into semantic HTTP errors inside a dependency.
|
|
- `backend/app/api/routes/items.py:53-57` uses 404 and 403 in the read handler.
|
|
- `backend/app/api/routes/items.py:86-95` repeats the same boundary pattern on update before applying the mutation.
|
|
- `backend/app/api/routes/items.py:106-112` repeats it on delete before committing the write.
|
|
|
|
### Pydantic and Starlette
|
|
|
|
- `pydantic/docs/index.md:109-152` shows invalid input producing structured validation errors rather than ad hoc strings.
|
|
- `starlette/tests/test_applications.py:218-238` asserts 404, custom 405, and 500 responses at the HTTP layer, reinforcing that response behavior is a framework contract, not just an implementation detail.
|
|
- `starlette/applications.py:35-48` separates middleware and exception handlers in application setup, which is part of why centralized formatting works cleanly.
|
|
|
|
## Bottom line
|
|
|
|
Routes and dependencies should express *HTTP meaning*.
|
|
|
|
Exception handlers should own *HTTP formatting*.
|
|
|
|
Core logic should stay framework-agnostic until the boundary translates it.
|