#FastAPI异常处理完全指南
📂 所属阶段:第二阶段 — 进阶黑科技(核心篇)
🔗 相关章节:FastAPI中间件应用 · FastAPI APIRouter模块化
#目录
#FastAPI异常处理机制
#FastAPI的异常体系
FastAPI继承了Starlette的异常处理机制,形成了完整的异常处理体系:
HTTPException(业务异常)
├── 400 Bad Request
├── 401 Unauthorized
├── 403 Forbidden
├── 404 Not Found
├── 422 Validation Error
└── 自定义 5xx
RequestValidationError(请求验证异常)
StarletteHTTPException(基类)
Python 内置异常(ValueError, KeyError, etc.)#抛出HTTPException
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"1": "Apple", "2": "Banana", "3": "Orange"}
@app.get("/items/{item_id}")
async def get_item(item_id: str):
if item_id not in items:
# 抛出 HTTPException,FastAPI 自动转为 JSON 响应
raise HTTPException(
status_code=404,
detail=f"Item {item_id} not found", # 自定义错误信息
headers={"X-Error": "ItemMissing"}
)
return {"item_id": item_id, "name": items[item_id]}FastAPI 会自动将其转为:
{
"detail": "Item 99 not found"
}#异常处理的核心原则
- 可预测性:客户端能预期错误响应格式
- 安全性:生产环境不暴露敏感信息
- 可追溯性:错误发生时能快速定位问题
- 一致性:所有API接口使用统一的错误格式
#全局异常处理器
#为什么需要全局处理?
- 路由中未捕获的异常会导致 500 Internal Server Error
- 不同路由的异常格式不统一
- 线上暴露异常栈信息有安全风险
#注册全局异常处理器
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import traceback
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
# ── 1. 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),
}
)
# ── 2. 请求验证异常处理器(参数校验失败)─────────────
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"])
errors.append({"field": field, "message": error["msg"]})
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "参数校验失败",
"errors": errors,
}
)
# ── 3. 通用异常处理器(兜底)───────────────────────
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# 生产环境:记录日志,返回通用错误
logger.error(f"Unhandled error: {exc}\n{traceback.format_exc()}")
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": "服务器内部错误,请稍后重试",
# 开发环境可以返回更多信息:
# "detail": str(exc),
# "traceback": traceback.format_exc(),
}
)#异常处理器的执行顺序
# 1. 首先检查最具体的异常类型
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(
status_code=400,
content={"code": 400, "message": f"值错误: {str(exc)}"}
)
# 2. 然后是更一般的异常类型
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content={"code": 500, "message": "未知错误"}
)
# 注意:上面的general_exception_handler永远不会被触发
# 因为所有的ValueError都已经被第一个处理器处理了
# 所以要注意异常处理器的注册顺序#自定义业务异常类
#创建统一的自定义异常
# exceptions.py — 统一的自定义异常
from typing import Optional, Dict, Any
from fastapi import HTTPException
class AppException(HTTPException):
"""基础业务异常"""
def __init__(self,
status_code: int = 400,
message: str = "Bad Request",
detail: Optional[str] = None,
headers: Optional[Dict[str, Any]] = None,
error_code: Optional[str] = None):
super().__init__(
status_code=status_code,
detail=detail or message,
headers=headers
)
self.message = message
self.error_code = error_code
class NotFoundException(AppException):
"""资源不存在"""
def __init__(self, resource: str, identifier: str):
super().__init__(
status_code=404,
message=f"{resource} '{identifier}' 不存在",
detail=f"{resource} '{identifier}' 不存在",
error_code="RESOURCE_NOT_FOUND"
)
class UnauthorizedException(AppException):
"""未授权"""
def __init__(self, reason: str = "请先登录"):
super().__init__(
status_code=401,
message=reason,
detail=reason,
error_code="UNAUTHORIZED",
headers={"WWW-Authenticate": "Bearer"}
)
class ForbiddenException(AppException):
"""权限不足"""
def __init__(self, reason: str = "权限不足,无法访问该资源"):
super().__init__(
status_code=403,
message=reason,
detail=reason,
error_code="FORBIDDEN"
)
class RateLimitException(AppException):
"""请求过于频繁"""
def __init__(self, retry_after: int = 60, reason: str = "请求过于频繁,请稍后重试"):
super().__init__(
status_code=429,
message=reason,
detail=reason,
error_code="RATE_LIMIT_EXCEEDED"
)
self.retry_after = retry_after
class ValidationException(AppException):
"""参数验证异常"""
def __init__(self, field: str, message: str):
super().__init__(
status_code=422,
message=f"参数验证失败: {field}",
detail=message,
error_code="VALIDATION_ERROR"
)#注册业务异常处理器
# 在应用中注册自定义异常处理器
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
content = {
"code": exc.status_code,
"message": exc.message,
"error_code": exc.error_code,
"path": str(request.url.path),
"timestamp": datetime.utcnow().isoformat()
}
if hasattr(exc, 'retry_after'):
content["retry_after"] = exc.retry_after
response = JSONResponse(status_code=exc.status_code, content=content)
if isinstance(exc, RateLimitException):
response.headers["Retry-After"] = str(exc.retry_after)
return response#在业务逻辑中使用自定义异常
from exceptions import NotFoundException, UnauthorizedException, ValidationException
@app.get("/users/{user_id}")
async def get_user(user_id: int):
if user_id <= 0:
raise ValidationException("user_id", "用户ID必须为正整数")
user = await db.find_user(user_id)
if not user:
raise NotFoundException("用户", str(user_id))
return user
@app.delete("/articles/{article_id}")
async def delete_article(article_id: int, current_user: dict = Depends(get_current_user)):
article = await db.find_article(article_id)
if not article:
raise NotFoundException("文章", str(article_id))
if article.author_id != current_user["id"]:
raise UnauthorizedException("无权删除他人文章")
await db.delete_article(article_id)
return {"deleted": article_id, "message": "删除成功"}#统一响应格式
#响应模型封装
from pydantic import BaseModel
from typing import TypeVar, Generic, Optional, Any, List, Dict
from datetime import datetime
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
"""统一响应格式"""
code: int = 200
message: str = "success"
data: Optional[T] = None
timestamp: str = datetime.utcnow().isoformat()
path: Optional[str] = None
error: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"code": 200,
"message": "success",
"data": {"id": 1, "name": "Alice"},
"timestamp": "2024-01-15T10:30:00Z"
}
}
class ApiError(BaseModel):
"""错误响应格式"""
code: int
message: str
error_code: Optional[str] = None
path: str
timestamp: str = datetime.utcnow().isoformat()
details: Optional[Dict[str, Any]] = None
def success_response(data: Any = None, message: str = "success", path: str = None) -> ApiResponse:
"""成功响应"""
return ApiResponse(
code=200,
message=message,
data=data,
path=path
)
def error_response(code: int,
message: str,
error_code: str = None,
path: str = None,
details: Dict[str, Any] = None) -> ApiError:
"""错误响应"""
return ApiError(
code=code,
message=message,
error_code=error_code,
path=path,
details=details
)#在路由中使用统一响应格式
from fastapi import APIRouter
from typing import List
router = APIRouter()
@router.get("/users", response_model=ApiResponse[List[dict]])
async def list_users():
try:
users = await db.query_users()
return success_response(data=users, message="获取用户列表成功")
except Exception as e:
logger.error(f"Failed to query users: {e}")
return error_response(500, "获取用户列表失败")
@router.get("/users/{user_id}", response_model=ApiResponse[dict])
async def get_user(user_id: int):
try:
user = await db.find_user(user_id)
if not user:
return error_response(404, "用户不存在", "USER_NOT_FOUND")
return success_response(data=user, message="获取用户成功")
except ValueError:
return error_response(400, "用户ID格式错误", "INVALID_USER_ID")
except Exception as e:
logger.error(f"Failed to get user {user_id}: {e}")
return error_response(500, "获取用户失败", "INTERNAL_ERROR")#异常处理与日志
#结构化日志配置
import structlog
import logging
from pythonjsonlogger import jsonlogger
from datetime import datetime
# 配置结构化日志
def configure_logging():
# JSON格式的日志
json_formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(name)s %(levelname)s %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(json_formatter)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.INFO)
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.UnicodeDecoder(),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
configure_logging()
logger = structlog.get_logger()
@app.exception_handler(Exception)
async def log_exception(request: Request, exc: Exception):
logger.error(
"request_failed",
method=request.method,
path=str(request.url.path),
query_params=dict(request.query_params),
error_type=type(exc).__name__,
error_message=str(exc),
user_agent=request.headers.get("user-agent"),
ip_address=request.client.host,
traceback=traceback.format_exc() if logger.isEnabledFor(logging.DEBUG) else None,
)
return JSONResponse(
status_code=500,
content={"code": 500, "message": "Internal server error"}
)#按异常类型差异化处理
@app.exception_handler(Exception)
async def smart_exception_handler(request: Request, exc: Exception):
if isinstance(exc, AppException):
# 业务异常:直接返回业务定义的错误
logger.info(
"business_exception",
error_code=exc.error_code,
message=exc.message,
path=str(request.url.path)
)
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.message,
"error_code": exc.error_code,
"timestamp": datetime.utcnow().isoformat()
}
)
if isinstance(exc, ValueError):
# 业务参数错误 → 400
logger.warning(
"value_error",
error=str(exc),
path=str(request.url.path)
)
return JSONResponse(
status_code=400,
content={
"code": 400,
"message": str(exc),
"error_code": "VALUE_ERROR",
"timestamp": datetime.utcnow().isoformat()
}
)
if isinstance(exc, ConnectionError):
# 服务不可用 → 503
logger.error("service_unavailable", error=str(exc))
return JSONResponse(
status_code=503,
content={
"code": 503,
"message": "服务暂时不可用",
"error_code": "SERVICE_UNAVAILABLE",
"timestamp": datetime.utcnow().isoformat()
}
)
# 未知异常 → 500 + 记录
logger.exception(
"unhandled_error",
error=str(exc),
path=str(request.url.path),
traceback=traceback.format_exc()
)
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": "服务器错误",
"error_code": "INTERNAL_ERROR",
"timestamp": datetime.utcnow().isoformat()
}
)#404错误处理
#全局404处理器
from starlette.requests import Request
from starlette.responses import JSONResponse
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
if exc.status_code == 404:
logger.info(
"not_found",
path=str(request.url.path),
method=request.method
)
return JSONResponse(
status_code=404,
content={
"code": 404,
"message": f"API 路径 '{request.url.path}' 不存在",
"suggestion": "请检查 URL 是否正确,或访问 /docs 查看可用接口",
"timestamp": datetime.utcnow().isoformat()
}
)
# 其他 HTTP 异常走通用处理
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"timestamp": datetime.utcnow().isoformat()
}
)#自定义404响应格式
# 更详细的404处理
@app.exception_handler(StarletteHTTPException)
async def detailed_404_handler(request: Request, exc: StarletteHTTPException):
if exc.status_code == 404:
# 检查是否是API路径
if request.url.path.startswith("/api/"):
suggestion = "请检查API版本或端点路径是否正确"
else:
suggestion = "请检查URL路径是否正确"
return JSONResponse(
status_code=404,
content={
"code": 404,
"message": f"请求的资源不存在",
"requested_path": request.url.path,
"method": request.method,
"timestamp": datetime.utcnow().isoformat(),
"suggestion": suggestion,
"available_endpoints": [
"/api/v1/users",
"/api/v1/products",
"/api/v1/orders"
]
}
)
# 其他HTTP异常处理
return JSONResponse(
status_code=exc.status_code,
content={
"code": exc.status_code,
"message": exc.detail,
"timestamp": datetime.utcnow().isoformat()
}
)#验证错误详解
#RequestValidationError结构
# 触发422验证错误的不同方式
@app.get("/calculate")
async def calculate(x: int, y: int):
if y == 0:
raise HTTPException(status_code=400, detail="除数不能为零")
return {"result": x / y}
# GET /calculate?x=10&y=0 → 业务逻辑错误(HTTPException)
# GET /calculate?x=abc&y=2 → 参数类型验证错误(RequestValidationError)验证错误响应示例:
{
"code": 422,
"message": "参数校验失败",
"errors": [
{
"field": "query.x",
"message": "Input should be a valid integer, unable to parse string as an integer"
}
]
}#自定义验证错误消息
from pydantic import BaseModel, field_validator, ValidationError
from typing import Optional
class CreateUser(BaseModel):
name: str
email: str
age: int
phone: Optional[str] = None
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("用户名不能为空")
if len(v.strip()) < 2:
raise ValueError("用户名长度至少为2个字符")
return v.strip()
@field_validator("email")
@classmethod
def email_format(cls, v: str) -> str:
if "@" not in v or "." not in v:
raise ValueError("邮箱格式不正确")
return v.lower()
@field_validator("age")
@classmethod
def age_range(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError("年龄必须在 0-150 之间")
return v
@field_validator("phone")
@classmethod
def phone_format(cls, v: Optional[str]) -> Optional[str]:
if v is not None:
# 简单的手机号验证
import re
if not re.match(r"^1[3-9]\d{9}$", v):
raise ValueError("手机号格式不正确")
return v
@app.post("/users")
async def create_user(user: CreateUser):
return {"created": user.model_dump()}#处理嵌套模型验证错误
class Address(BaseModel):
street: str
city: str
zip_code: str
@field_validator("zip_code")
@classmethod
def zip_code_format(cls, v: str) -> str:
import re
if not re.match(r"^\d{6}$", v):
raise ValueError("邮编格式不正确,应为6位数字")
return v
class UserWithAddress(BaseModel):
name: str
address: Address
# 错误处理示例
@app.exception_handler(RequestValidationError)
async def enhanced_validation_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
loc = error["loc"]
field = " -> ".join(str(loc_part) for loc_part in loc)
errors.append({
"field": field,
"message": error["msg"],
"type": error["type"],
"input": error.get("input")
})
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "参数校验失败",
"validation_errors": errors,
"timestamp": datetime.utcnow().isoformat()
}
)#异常处理最佳实践
#推荐做法
✅ 推荐:
1. 自定义业务异常类,统一错误码和消息格式
2. 异常处理器返回统一的 JSON 结构
3. 生产环境不要暴露异常堆栈
4. 记录详细日志供排查
5. HTTPException 用于"预期内的错误"
6. 全局兜底异常处理器防止意外 500 泄漏
7. 区分客户端错误和服务器错误
8. 为不同类型的错误提供相应的错误码
9. 在异常中包含足够的上下文信息
10. 实现优雅降级机制#实现优雅的错误处理
# 完整的错误处理示例
from enum import Enum
class ErrorCode(str, Enum):
"""错误码枚举"""
# 通用错误
INTERNAL_ERROR = "INTERNAL_ERROR"
VALIDATION_ERROR = "VALIDATION_ERROR"
RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
UNAUTHORIZED = "UNAUTHORIZED"
FORBIDDEN = "FORBIDDEN"
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
# 业务错误
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS"
INVALID_CREDENTIALS = "INVALID_CREDENTIALS"
INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
class DetailedApiError(BaseModel):
"""详细错误响应"""
code: int
error_code: ErrorCode
message: str
details: Optional[Dict[str, Any]] = None
path: str
timestamp: str = datetime.utcnow().isoformat()
trace_id: Optional[str] = None # 用于追踪错误
# 为每个异常类型创建专门的处理器
@app.exception_handler(StarletteHTTPException)
async def standard_http_exception_handler(request: Request, exc: StarletteHTTPException):
error_map = {
400: ErrorCode.VALIDATION_ERROR,
401: ErrorCode.UNAUTHORIZED,
403: ErrorCode.FORBIDDEN,
404: ErrorCode.RESOURCE_NOT_FOUND,
429: ErrorCode.RATE_LIMIT_EXCEEDED,
}
error_code = error_map.get(exc.status_code, ErrorCode.INTERNAL_ERROR)
return JSONResponse(
status_code=exc.status_code,
content=DetailedApiError(
code=exc.status_code,
error_code=error_code,
message=exc.detail,
path=str(request.url.path),
details={"original_status": exc.status_code}
).model_dump()
)
@app.exception_handler(RequestValidationError)
async def detailed_validation_handler(request: Request, exc: RequestValidationError):
validation_errors = []
for error in exc.errors():
validation_errors.append({
"field": " -> ".join(str(loc) for loc in error["loc"]),
"message": error["msg"],
"type": error["type"]
})
return JSONResponse(
status_code=422,
content=DetailedApiError(
code=422,
error_code=ErrorCode.VALIDATION_ERROR,
message="请求参数验证失败",
details={"errors": validation_errors},
path=str(request.url.path)
).model_dump()
)#性能优化建议
#1. 异常处理的性能考虑
# 避免在正常业务流程中使用异常
# ❌ 错误:用异常控制流程
def get_user_safe(user_id: int):
try:
return db.get_user(user_id)
except NotFoundError:
return None
# ✅ 正确:先检查再操作
def get_user_optimized(user_id: int):
if db.user_exists(user_id):
return db.get_user(user_id)
return None#2. 日志性能优化
import asyncio
from concurrent.futures import ThreadPoolExecutor
# 使用异步日志记录避免阻塞
class AsyncLogger:
def __init__(self):
self.executor = ThreadPoolExecutor(max_workers=2)
async def log_error_async(self, message: str, **kwargs):
loop = asyncio.get_event_loop()
await loop.run_in_executor(
self.executor,
lambda: logger.error(message, **kwargs)
)
async_logger = AsyncLogger()
@app.exception_handler(Exception)
async def async_logging_handler(request: Request, exc: Exception):
# 异步记录日志,避免阻塞响应
await async_logger.log_error_async(
"unhandled_error",
error=str(exc),
path=str(request.url.path),
traceback=traceback.format_exc()
)
return JSONResponse(
status_code=500,
content={"code": 500, "message": "服务器错误"}
)#3. 异常缓存策略
from functools import lru_cache
import time
# 对于频繁发生的可预见错误,可以考虑缓存错误响应
class ErrorCache:
def __init__(self, ttl: int = 60):
self.cache = {}
self.ttl = ttl
def get(self, key: str):
if key in self.cache:
value, timestamp = self.cache[key]
if time.time() - timestamp < self.ttl:
return value
del self.cache[key]
return None
def set(self, key: str, value):
self.cache[key] = (value, time.time())
error_cache = ErrorCache()
@app.get("/users/{user_id}")
async def get_user_cached(user_id: int):
cache_key = f"user_not_found_{user_id}"
# 检查缓存的错误
cached_error = error_cache.get(cache_key)
if cached_error:
return JSONResponse(status_code=404, content=cached_error)
user = await db.find_user(user_id)
if not user:
error_response = {
"code": 404,
"message": f"用户 {user_id} 不存在",
"timestamp": datetime.utcnow().isoformat()
}
# 缓存错误响应,避免频繁查询
error_cache.set(cache_key, error_response)
return JSONResponse(status_code=404, content=error_response)
return user#常见陷阱与避坑指南
#陷阱1:异常处理器顺序错误
# ❌ 错误:通用异常处理器放在前面
@app.exception_handler(Exception)
async def general_handler(request: Request, exc: Exception):
return JSONResponse(500, {"error": "General error"})
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
# 这个处理器永远不会被执行
return JSONResponse(400, {"error": "Value error"})
# ✅ 正确:具体异常处理器放在前面
@app.exception_handler(ValueError)
async def value_error_handler(request: Request, exc: ValueError):
return JSONResponse(400, {"error": "Value error"})
@app.exception_handler(Exception)
async def general_handler(request: Request, exc: Exception):
return JSONResponse(500, {"error": "General error"})#陷阱2:在异常处理器中引发新异常
# ❌ 错误:异常处理器本身也抛出异常
@app.exception_handler(Exception)
async def bad_handler(request: Request, exc: Exception):
# 如果这里的操作失败,会导致双重异常
problematic_operation()
return JSONResponse(500, {"error": "Handled"})
# ✅ 正确:异常处理器应该是安全的
@app.exception_handler(Exception)
async def safe_handler(request: Request, exc: Exception):
try:
# 安全的日志记录
logger.error(f"Error occurred: {exc}")
except:
# 即使日志记录失败也不能影响响应
pass
return JSONResponse(500, {"error": "Internal server error"})#陷阱3:暴露敏感信息
# ❌ 错误:在生产环境中暴露详细错误信息
@app.exception_handler(Exception)
async def leaky_handler(request: Request, exc: Exception):
return JSONResponse(
500,
{
"error": str(exc),
"traceback": traceback.format_exc(), # 泄露敏感信息
"locals": locals() # 泄露局部变量
}
)
# ✅ 正确:生产环境隐藏敏感信息
@app.exception_handler(Exception)
async def secure_handler(request: Request, exc: Exception):
# 只在开发环境记录详细信息
if os.getenv("ENVIRONMENT") == "development":
logger.error(f"Error details: {exc}\n{traceback.format_exc()}")
else:
logger.error(f"Error occurred: {type(exc).__name__}")
return JSONResponse(
500,
{"error": "Internal server error"}
)#陷阱4:忽略异常上下文
# ❌ 错误:丢失异常上下文
@app.exception_handler(SomeException)
async def poor_context_handler(request: Request, exc: SomeException):
# 没有保留原始异常的上下文信息
return JSONResponse(500, {"error": "Something went wrong"})
# ✅ 正确:保留有用的上下文信息
@app.exception_handler(SomeException)
async def rich_context_handler(request: Request, exc: SomeException):
logger.error(
"Business exception occurred",
path=str(request.url.path),
method=request.method,
user_id=getattr(request.state, 'user_id', 'unknown'),
error=str(exc)
)
return JSONResponse(
500,
{
"error": "Operation failed",
"code": "BUSINESS_ERROR",
"timestamp": datetime.utcnow().isoformat()
}
)#相关教程
#总结
| 异常类型 | 触发场景 | 推荐处理方式 |
|---|---|---|
HTTPException | 业务逻辑(资源不存在、权限不足) | 使用自定义业务异常类 |
RequestValidationError | 参数校验失败 | 统一验证错误处理器 |
Exception | 所有未捕获异常 | 全局兜底异常处理器 |
自定义 AppException | 业务特定错误 | 分类处理,统一格式 |
💡 核心原则:异常处理的目标是让客户端收到可预期的错误响应,同时让服务端记录足够排查问题的日志,并在生产环境中保护敏感信息不被泄露。
FastAPI的异常处理机制为企业级应用提供了强大的错误处理能力,通过合理的异常分类、统一的响应格式和完善的日志记录,可以构建出稳定可靠的API服务。
#1. FastAPI 异常处理机制
#1.1 FastAPI 的异常体系
HTTPException(业务异常)
├── 400 Bad Request
├── 401 Unauthorized
├── 403 Forbidden
├── 404 Not Found
├── 422 Validation Error
└── 自定义 5xx
RequestValidationError(请求验证异常)
StarletteHTTPException(基类)
Python 内置异常(ValueError, KeyError, etc.)#1.2 抛出 HTTPException
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"1": "Apple", "2": "Banana", "3": "Orange"}
@app.get("/items/{item_id}")
async def get_item(item_id: str):
if item_id not in items:
# 抛出 HTTPException,FastAPI 自动转为 JSON 响应
raise HTTPException(
status_code=404,
detail=f"Item {item_id} not found", # 自定义错误信息
headers={"X-Error": "ItemMissing"}
)
return {"item_id": item_id, "name": items[item_id]}FastAPI 会自动将其转为:
{
"detail": "Item 99 not found"
}#2. 全局异常处理器
#2.1 为什么需要全局处理?
- 路由中未捕获的异常会导致 500 Internal Server Error
- 不同路由的异常格式不统一
- 线上暴露异常栈信息有安全风险
#2.2 注册全局异常处理器
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import traceback
import logging
logger = logging.getLogger(__name__)
app = FastAPI()
# ── 1. 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),
}
)
# ── 2. 请求验证异常处理器(参数校验失败)─────────────
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"])
errors.append({"field": field, "message": error["msg"]})
return JSONResponse(
status_code=422,
content={
"code": 422,
"message": "参数校验失败",
"errors": errors,
}
)
# ── 3. 通用异常处理器(兜底)───────────────────────
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
# 生产环境:记录日志,返回通用错误
logger.error(f"Unhandled error: {exc}\n{traceback.format_exc()}")
return JSONResponse(
status_code=500,
content={
"code": 500,
"message": "服务器内部错误,请稍后重试",
# 开发环境可以返回更多信息:
# "detail": str(exc),
# "traceback": traceback.format_exc(),
}
)#2.3 自定义业务异常类
# exceptions.py — 统一的自定义异常
class AppException(Exception):
"""基础业务异常"""
def __init__(self, message: str, code: int = 400, details: dict = None):
self.message = message
self.code = code
self.details = details or {}
super().__init__(message)
class NotFoundException(AppException):
"""资源不存在"""
def __init__(self, resource: str, identifier: str):
super().__init__(
message=f"{resource} '{identifier}' 不存在",
code=404,
details={"resource": resource, "identifier": identifier}
)
class UnauthorizedException(AppException):
"""未授权"""
def __init__(self, reason: str = "请先登录"):
super().__init__(message=reason, code=401)
class ForbiddenException(AppException):
"""权限不足"""
def __init__(self):
super().__init__(message="权限不足,无法访问该资源", code=403)
class RateLimitException(AppException):
"""请求过于频繁"""
def __init__(self, retry_after: int = 60):
super().__init__(
message="请求过于频繁,请稍后重试",
code=429,
details={"retry_after": retry_after}
)# 注册业务异常处理器
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
content = {
"code": exc.code,
"message": exc.message,
}
if exc.details:
content["details"] = exc.details
response = JSONResponse(status_code=exc.code, content=content)
if isinstance(exc, RateLimitException):
response.headers["Retry-After"] = str(exc.details.get("retry_after", 60))
return response#2.4 使用自定义异常
from exceptions import NotFoundException, UnauthorizedException
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await db.find_user(user_id)
if not user:
raise NotFoundException("用户", str(user_id))
return user
@app.delete("/articles/{article_id}")
async def delete_article(article_id: int, token: str = Header(...)):
user = await auth.verify(token)
article = await db.find_article(article_id)
if article.author_id != user.id:
raise UnauthorizedException("无权删除他人文章")
await db.delete(article)
return {"deleted": article_id}#3. 统一响应格式
#3.1 响应模型封装
from pydantic import BaseModel
from typing import TypeVar, Generic, Optional, Any
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
"""统一响应格式"""
code: int = 200
message: str = "success"
data: Optional[T] = None
error: Optional[str] = None
class Config:
json_schema_extra = {
"example": {
"code": 200,
"message": "success",
"data": {"id": 1, "name": "Alice"}
}
}
def success_response(data: Any = None, message: str = "success"):
return ApiResponse(code=200, message=message, data=data)
def error_response(code: int, message: str, error: str = None):
return ApiResponse(code=code, message=message, error=error)#3.2 在路由中使用
from fastapi import APIRouter
from typing import List
router = APIRouter()
@router.get("/users", response_model=ApiResponse[List[dict]])
async def list_users():
users = await db.query_users()
return success_response(data=users, message="获取用户列表成功")
@router.get("/users/{user_id}", response_model=ApiResponse[dict])
async def get_user(user_id: int):
user = await db.find_user(user_id)
if not user:
return error_response(404, "用户不存在")
return success_response(data=user)#4. 异常处理与日志
#4.1 结构化日志
import structlog
import logging
# 配置结构化日志
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer(),
],
wrapper_class=structlog.stdlib.BoundLogger,
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
logger = structlog.get_logger()
@app.exception_handler(Exception)
async def log_exception(request: Request, exc: Exception):
logger.error(
"request_failed",
method=request.method,
path=str(request.url.path),
error_type=type(exc).__name__,
error_message=str(exc),
traceback=traceback.format_exc(),
)
return JSONResponse(
status_code=500,
content={"code": 500, "message": "Internal server error"}
)#4.2 按异常类型差异化处理
@app.exception_handler(Exception)
async def smart_exception_handler(request: Request, exc: Exception):
if isinstance(exc, AppException):
# 业务异常:直接返回业务定义的错误
return JSONResponse(
status_code=exc.code,
content={"code": exc.code, "message": exc.message}
)
if isinstance(exc, ValueError):
# 业务参数错误 → 400
return JSONResponse(400, {"code": 400, "message": str(exc)})
if isinstance(exc, ConnectionError):
# 服务不可用 → 503
logger.error("service_unavailable", error=str(exc))
return JSONResponse(503, {"code": 503, "message": "服务暂时不可用"})
# 未知异常 → 500 + 记录
logger.exception("unhandled_error")
return JSONResponse(500, {"code": 500, "message": "服务器错误"})#5. 404 处理
#5.1 全局 404 处理器
from starlette.requests import Request
from starlette.responses import JSONResponse
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
if exc.status_code == 404:
return JSONResponse(
status_code=404,
content={
"code": 404,
"message": f"API 路径 '{request.url.path}' 不存在",
"suggestion": "请检查 URL 是否正确,或访问 /docs 查看可用接口"
}
)
# 其他 HTTP 异常走通用处理
return JSONResponse(
status_code=exc.status_code,
content={"code": exc.status_code, "message": exc.detail}
)#6. 验证错误详解
#6.1 RequestValidationError 结构
# 触发 422 验证错误
@app.get("/calculate")
async def calculate(x: int, y: int):
return {"result": x / y}
# GET /calculate?x=10&y=0 → 除零错误(Python 异常,非验证异常)
# GET /calculate?x=abc&y=2 → 参数类型验证错误(RequestValidationError)验证错误响应示例:
{
"code": 422,
"message": "参数校验失败",
"errors": [
{
"field": "body.price",
"message": "Input should be greater than or equal to 0"
}
]
}#6.2 自定义验证错误消息
from pydantic import BaseModel, field_validator
class CreateUser(BaseModel):
name: str
email: str
age: int
@field_validator("name")
@classmethod
def name_must_not_be_empty(cls, v: str) -> str:
if not v.strip():
raise ValueError("用户名不能为空")
return v.strip()
@field_validator("email")
@classmethod
def email_format(cls, v: str) -> str:
if "@" not in v:
raise ValueError("邮箱格式不正确")
return v.lower()
@field_validator("age")
@classmethod
def age_range(cls, v: int) -> int:
if v < 0 or v > 150:
raise ValueError("年龄必须在 0-150 之间")
return v
@app.post("/users")
async def create_user(user: CreateUser):
return {"created": user.model_dump()}#7. 异常处理最佳实践
✅ 推荐:
1. 自定义业务异常类,统一错误码和消息格式
2. 异常处理器返回统一的 JSON 结构
3. 生产环境不要暴露异常堆栈
4. 记录详细日志供排查
5. HTTPException 用于"预期内的错误"
6. 全局兜底异常处理器防止意外 500 泄漏
❌ 避免:
1. 路由中用 try/except 吞掉所有异常但不处理
2. 在 except 中 print 而不记录日志
3. 混用不同的错误响应格式
4. 过于宽泛的 except Exception#8. 小结
| 异常类型 | 触发场景 | FastAPI 默认响应 |
|---|---|---|
HTTPException | 业务逻辑(资源不存在、权限不足) | {"detail": "..."} |
RequestValidationError | 参数校验失败 | 详细错误位置列表 |
Exception | 所有未捕获异常 | 500(需自定义兜底) |
自定义 AppException | 业务特定错误 | 自定义结构 |
💡 核心原则:异常处理的目标是让客户端收到可预期的错误响应,同时让服务端记录足够排查问题的日志。
🔗 扩展阅读

