Complete guide to FastAPIexception-handling
📂 Stage: Stage 2 - Advanced Black Technology (Core)
🔗 Related chapters: FastAPImiddleware-application · FastAPI APIRouter模块化
1. FastAPIexception-handling basics
1.1 Exception architecture
FastAPI's exception mechanism is inherited from Starlette and covers three core scenarios: business expected errors, request verification errors and unknown unknown errors. Understanding this layered structure is the first step in designing a robust error handling system.
StarletteHTTPException(基类)
├── HTTPException(业务可控异常)
│ ├── 400/401/403/404/422/自定义5xx
│ └── 可附加 headers/自定义 detail
└── RequestValidationError(Pydantic/请求自动验证错误)
Python 内置异常(ValueError/KeyError/...)
└── 未路由捕获时 → 500 Internal Server Error(需兜底)
1.2 Basic HTTPException usage
The most common way is to throw it directlyHTTPException, FastAPI will automatically convert it to a JSON response.
from fastapi import FastAPI, HTTPException
app = FastAPI()
fruits = {"1": "Apple", "2": "Banana", "3": "Orange"}
@app.get("/fruits/{fruit_id}")
async def get_fruit(fruit_id: str):
if fruit_id not in fruits:
raise HTTPException(
status_code=404,
detail=f"Fruit {fruit_id} not found",
headers={"X-Error-Type": "FruitMissing"} # 可选附加header
)
return {"fruit_id": fruit_id, "name": fruits[fruit_id]}
The JSON returned by the interface is very concise:
{"detail": "Fruit 99 not found"}
2. Global exception-handling device
If every route usestry/except, not only is the code redundant, it can also easily lead to inconsistent response formats and even leak sensitive stack information. Global exception-handler is the preferred solution for production environments. It can uniformly capture all unhandled exceptions and return error information in a standard format.
2.1 Core processor system
Three types of processors are registered below, covering 99% of scenarios:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import logging
import traceback
from datetime import datetime
logger = logging.getLogger(__name__)
app = FastAPI()
# 1. 处理 StarletteHTTPException(包含 FastAPI 的 HTTPException)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"path": str(request.url.path),
"timestamp": datetime.utcnow().isoformat()
}
)
# 2. 处理请求验证失败(Pydantic 自动校验未通过)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for err in exc.errors():
field = ".".join(str(loc) for loc in err["loc"]) # 将错误字段路径格式化为字符串
errors.append({"field": field, "msg": err["msg"]})
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "请求参数校验失败",
"errors": errors,
"path": str(request.url.path),
"timestamp": datetime.utcnow().isoformat()
}
)
# 3. 兜底通用异常(任何未预期的错误)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# 生产环境仅记录日志,不向前端暴露 traceback
logger.error(
"Unhandled Server Error",
path=str(request.url.path),
error_type=type(exc).__name__,
error_msg=str(exc),
traceback=traceback.format_exc() if app.debug else None
)
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": "服务器内部错误,请稍后联系管理员",
"path": str(request.url.path),
"timestamp": datetime.utcnow().isoformat()
}
)
2.2 Pay attention to the processor registration sequence
You must first register "specific exceptions" and then register "general exceptions", otherwise the specific handler will never be triggered:
# ❌ 错误顺序:通用处理器在前
@app.exception_handler(Exception)
async def wrong_order(...): ...
@app.exception_handler(ValueError)
async def never_triggered(...): ...
# ✅ 正确顺序:具体处理器在前
@app.exception_handler(ValueError)
async def value_error(...): ...
@app.exception_handler(Exception)
async def general(...): ...
3. Customized business exception class
Use directlyHTTPExceptionHandwritten status codes and error messages not only cause a lot of duplication of work, but are also difficult to manage uniformly. Customized business exception system can make the code clearer and errors more semantic.
3.1 Define core exception classes
Newexceptions.pyFiles to centrally manage all business exceptions:
from fastapi import HTTPException
from typing import Optional, Dict, Any
class AppException(HTTPException):
"""基础业务异常,封装 error_code 等扩展字段"""
def __init__(
self,
status_code: int = 400,
message: str = "Bad Request",
error_code: Optional[str] = None,
headers: Optional[Dict[str, Any]] = None
):
super().__init__(
status_code=status_code,
detail=message,
headers=headers
)
self.app_code = error_code or f"E{status_code}"
class NotFoundException(AppException):
"""资源不存在"""
def __init__(self, resource: str, identifier: str):
super().__init__(
status_code=404,
message=f"{resource} '{identifier}' 不存在",
error_code="RESOURCE_NOT_FOUND"
)
class UnauthorizedException(AppException):
"""未授权"""
def __init__(self, reason: str = "请先登录"):
super().__init__(
status_code=401,
message=reason,
error_code="UNAUTHORIZED",
headers={"WWW-Authenticate": "Bearer"}
)
class RateLimitException(AppException):
"""请求限流"""
def __init__(self, retry_after: int = 60):
super().__init__(
status_code=429,
message=f"请求过于频繁,请{retry_after}秒后重试",
error_code="RATE_LIMIT_EXCEEDED"
)
self.retry_after = retry_after
3.2 Register business exception-handler
forAppExceptionRegister the handler separately so thatapp_codeWait for the custom information to be returned to the front end:
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
content = {
"code": exc.status_code,
"app_code": exc.app_code,
"message": exc.detail,
"path": str(request.url.path),
"timestamp": datetime.utcnow().isoformat()
}
response = JSONResponse(status_code=exc.status_code, content=content)
# 针对限流异常,添加 Retry-After 头
if isinstance(exc, RateLimitException):
response.headers["Retry-After"] = str(exc.retry_after)
return response
3.3 Use in business logic
Custom exceptions make business code clear at a glance:
from exceptions import NotFoundException, UnauthorizedException
@app.delete("/articles/{article_id}")
async def delete_article(article_id: int, current_user: dict = Depends(get_current_user)):
# 模拟数据库查询
article = {"id": article_id, "author_id": 1}
if not article:
raise NotFoundException("文章", str(article_id))
if article["author_id"] != current_user["id"]:
raise UnauthorizedException("无权删除他人文章")
return {"deleted": article_id, "message": "删除成功"}
Not only error responses, successful responses should also adopt a unified format to reduce front-end judgment logic.
4.1 Encapsulate unified response model
existschemas.pyUse Pydantic to define the return structure:
from pydantic import BaseModel, Field
from typing import TypeVar, Generic, Optional
from datetime import datetime
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
"""通用成功响应"""
code: int = 200
message: str = "success"
data: Optional[T] = None
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
class ApiError(BaseModel):
"""通用错误响应"""
code: int
app_code: str
message: str
path: str
timestamp: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
# 快捷构造方法
def success_response(data: Optional[T] = None, message: str = "success") -> ApiResponse[T]:
return ApiResponse(data=data, message=message)
5. Common Traps and Avoidance
In a production environment, never usetracebackOr the database query statement is returned to the client:
# ❌ 错误:直接返回堆栈
@app.exception_handler(Exception)
async def leaky_handler(...):
return JSONResponse(500, {"traceback": traceback.format_exc()})
# ✅ 正确:仅开发模式包含调试信息
@app.exception_handler(Exception)
async def safe_handler(...):
content = {"code": 500, "message": "服务器内部错误"}
if app.debug:
content["traceback"] = traceback.format_exc()
return JSONResponse(500, content)
❌ Trap 2: Use exceptions to control normal processes
Exception creation and capture have performance overhead, Do not use it to replace ordinary conditional judgments:
# ❌ 错误:滥用异常
def get_fruit_safe(fruit_id: str):
try:
return fruits[fruit_id]
except KeyError:
return None
# ✅ 正确:使用 get() 等内置方法
def get_fruit_optimized(fruit_id: str):
return fruits.get(fruit_id)
6. Summary
💡 Core principles: The client receives a predictable response, and the server records traceable logs.
- [FastAPI 官方exception-handling文档](https://fastapi.tiangolo.com/tutorial/handling-errors/)
- [Pydantic 自定义验证器](https://docs.pydantic.dev/latest/concepts/validators/)
- [Starlette exception-handling机制](https://www.starlette.io/exceptions/)