调试与边界:错误处理、日志、测试
错误处理
1. HTTPException
python
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": {"name": "Foo"}}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return items[item_id]测试:
bash
curl http://localhost:8000/items/bar
# 输出: {"detail":"Item not found"}
# 状态码: 4042. 自定义异常处理器
python
from fastapi import Request
from fastapi.responses import JSONResponse
class CustomException(Exception):
def __init__(self, name: str):
self.name = name
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something wrong."},
)
@app.get("/custom/{name}")
async def custom(name: str):
if name == "error":
raise CustomException(name=name)
return {"name": name}3. 验证错误处理
python
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)日志记录
配置日志
python
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
logger.info(f"Fetching item {item_id}")
try:
item = get_item(item_id)
logger.info(f"Item {item_id} found")
return item
except Exception as e:
logger.error(f"Error fetching item {item_id}: {str(e)}")
raise请求日志中间件
python
import time
@app.middleware("http")
async def log_requests(request: Request, call_next):
start_time = time.time()
logger.info(f"Request: {request.method} {request.url}")
response = await call_next(request)
process_time = time.time() - start_time
logger.info(f"Response: {response.status_code} - {process_time:.2f}s")
return response边界情况
1. 空值和可选参数
python
from typing import Optional
@app.get("/items/")
async def read_items(
q: Optional[str] = None,
skip: int = 0,
limit: int = 100
):
# q 可以是 None
if q:
# 有查询参数
items = search_items(q)
else:
# 无查询参数
items = get_all_items()
return items[skip:skip + limit]2. 类型转换边界
python
from pydantic import validator
class Item(BaseModel):
name: str
price: float
@validator('price')
def price_must_be_positive(cls, v):
if v <= 0:
raise ValueError('价格必须大于 0')
return v
@validator('name')
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError('名称不能为空')
return v.strip()3. 大文件上传
python
from fastapi import File, UploadFile
@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
# 检查文件大小
contents = await file.read()
if len(contents) > 10 * 1024 * 1024: # 10 MB
raise HTTPException(status_code=413, detail="File too large")
# 检查文件类型
if file.content_type not in ["image/jpeg", "image/png"]:
raise HTTPException(status_code=415, detail="Unsupported file type")
return {"filename": file.filename, "size": len(contents)}测试
使用 TestClient
python
from fastapi.testclient import TestClient
client = TestClient(app)
def test_read_main():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello World"}
def test_read_item():
response = client.get("/items/foo")
assert response.status_code == 200
assert response.json() == {"name": "Foo"}
def test_read_item_not_found():
response = client.get("/items/bar")
assert response.status_code == 404
assert response.json() == {"detail": "Item not found"}
def test_create_item_invalid():
response = client.post("/items/", json={"name": "", "price": -10})
assert response.status_code == 422调试技巧
1. 启用调试模式
python
app = FastAPI(debug=True)2. 查看请求详情
python
from fastapi import Request
@app.get("/debug")
async def debug(request: Request):
return {
"method": request.method,
"url": str(request.url),
"headers": dict(request.headers),
"query_params": dict(request.query_params),
"path_params": request.path_params,
"client": request.client.host if request.client else None,
}3. 性能分析
python
import time
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response实验:完整错误处理
创建一个健壮的 API 端点:
python
from typing import Optional
import logging
logger = logging.getLogger(__name__)
@app.get("/safe-items/{item_id}")
async def safe_read_item(item_id: int, q: Optional[str] = None):
try:
logger.info(f"Fetching item {item_id}")
# 参数验证
if item_id < 0:
raise HTTPException(status_code=400, detail="Invalid item ID")
# 业务逻辑
item = get_item_from_db(item_id)
if not item:
logger.warning(f"Item {item_id} not found")
raise HTTPException(status_code=404, detail="Item not found")
# 搜索过滤
if q and q.lower() not in item.name.lower():
return {"message": "No match"}
logger.info(f"Successfully returned item {item_id}")
return item
except HTTPException:
raise
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")验收标准
✅ 404、422、500 等错误正确返回
✅ 日志记录关键操作和错误
✅ 边界情况(空值、负数、大文件)正确处理
✅ 测试覆盖正常和异常路径
✅ 调试信息清晰可追踪
下一步:查看练习答案验证理解。