Skip to content

FastAPI 请求和响应

概述

HTTP请求和响应是Web API的核心。FastAPI提供了强大的请求处理和响应生成机制,支持自动验证、序列化、文档生成等功能。本章将深入探讨如何处理各种类型的请求数据和定制响应格式。

📥 请求体处理

Pydantic模型请求体

python
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
from enum import Enum

app = FastAPI()

class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    URGENT = "urgent"

class TaskCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=200, description="任务标题")
    description: Optional[str] = Field(None, max_length=1000, description="任务描述")
    priority: Priority = Field(Priority.MEDIUM, description="任务优先级")
    due_date: Optional[datetime] = Field(None, description="截止日期")
    tags: List[str] = Field(default_factory=list, description="任务标签")
    estimated_hours: Optional[float] = Field(None, ge=0.1, le=1000, description="预估工时")

    @validator('tags')
    def validate_tags(cls, v):
        if len(v) > 10:
            raise ValueError('标签数量不能超过10个')
        return [tag.strip().lower() for tag in v if tag.strip()]

    @validator('due_date')
    def validate_due_date(cls, v):
        if v and v < datetime.now():
            raise ValueError('截止日期不能是过去的时间')
        return v

class TaskResponse(BaseModel):
    id: int
    title: str
    description: Optional[str]
    priority: Priority
    due_date: Optional[datetime]
    tags: List[str]
    estimated_hours: Optional[float]
    created_at: datetime
    updated_at: datetime
    is_completed: bool = False

    class Config:
        from_attributes = True

# 创建任务
@app.post("/tasks/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
async def create_task(task: TaskCreate):
    # 模拟创建任务
    task_data = task.dict()
    task_data.update({
        "id": 1,
        "created_at": datetime.now(),
        "updated_at": datetime.now(),
        "is_completed": False
    })
    
    return TaskResponse(**task_data)

嵌套模型

python
class Address(BaseModel):
    street: str = Field(..., min_length=1, max_length=200)
    city: str = Field(..., min_length=1, max_length=100)
    state: str = Field(..., min_length=2, max_length=50)
    postal_code: str = Field(..., regex=r"^\d{5}(-\d{4})?$")
    country: str = Field(default="US", max_length=50)

class Contact(BaseModel):
    email: str = Field(..., regex=r"^[^@]+@[^@]+\.[^@]+$")
    phone: Optional[str] = Field(None, regex=r"^\+?1?\d{9,15}$")

class UserCreate(BaseModel):
    username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_]+$")
    full_name: str = Field(..., min_length=1, max_length=100)
    address: Address
    contact: Contact
    preferences: Optional[dict] = Field(default_factory=dict)

    @validator('preferences')
    def validate_preferences(cls, v):
        allowed_keys = {'theme', 'language', 'notifications', 'timezone'}
        if not all(key in allowed_keys for key in v.keys()):
            raise ValueError(f'preferences只能包含: {allowed_keys}')
        return v

@app.post("/users/", response_model=dict)
async def create_user(user: UserCreate):
    return {
        "message": "用户创建成功",
        "user": user.dict(),
        "summary": {
            "username": user.username,
            "location": f"{user.address.city}, {user.address.state}",
            "contact_email": user.contact.email
        }
    }

多个请求体参数

python
class Item(BaseModel):
    name: str
    price: float
    description: Optional[str] = None

class User(BaseModel):
    username: str
    full_name: str

class Metadata(BaseModel):
    source: str = "api"
    timestamp: datetime = Field(default_factory=datetime.now)

@app.post("/items/{item_id}")
async def update_item_with_user(
    item_id: int,
    item: Item,
    user: User,
    metadata: Metadata,
    importance: int = Field(..., ge=1, le=5, description="重要程度 1-5")
):
    return {
        "item_id": item_id,
        "item": item.dict(),
        "user": user.dict(),
        "metadata": metadata.dict(),
        "importance": importance,
        "operation": "update_with_context"
    }

📤 响应模型

基础响应模型

python
from typing import Union

class ErrorResponse(BaseModel):
    error: str
    message: str
    details: Optional[dict] = None

class SuccessResponse(BaseModel):
    success: bool = True
    message: str
    data: Optional[dict] = None

class PaginatedResponse(BaseModel):
    items: List[dict]
    total: int
    page: int
    per_page: int
    pages: int

    @validator('page')
    def validate_page(cls, v, values):
        if 'pages' in values and v > values['pages']:
            raise ValueError('页码不能超过总页数')
        return v

# 使用Union类型的响应
@app.get("/items/{item_id}", response_model=Union[TaskResponse, ErrorResponse])
async def get_item(item_id: int):
    if item_id <= 0:
        return ErrorResponse(
            error="invalid_id",
            message="物品ID必须大于0",
            details={"provided_id": item_id}
        )
    
    # 模拟获取物品
    if item_id == 999:
        return ErrorResponse(
            error="not_found",
            message="未找到指定的物品"
        )
    
    return TaskResponse(
        id=item_id,
        title="示例任务",
        description="这是一个示例任务",
        priority=Priority.MEDIUM,
        created_at=datetime.now(),
        updated_at=datetime.now()
    )

响应模型继承

python
class BaseResponse(BaseModel):
    timestamp: datetime = Field(default_factory=datetime.now)
    api_version: str = "1.0"

class UserResponse(BaseResponse):
    id: int
    username: str
    email: str
    is_active: bool

class UserListResponse(BaseResponse):
    users: List[UserResponse]
    total_count: int

class UserDetailResponse(UserResponse):
    full_name: str
    created_at: datetime
    last_login: Optional[datetime]
    profile: Optional[dict]

@app.get("/users/", response_model=UserListResponse)
async def list_users():
    return UserListResponse(
        users=[
            UserResponse(id=1, username="alice", email="alice@example.com", is_active=True),
            UserResponse(id=2, username="bob", email="bob@example.com", is_active=False)
        ],
        total_count=2
    )

@app.get("/users/{user_id}", response_model=UserDetailResponse)
async def get_user_detail(user_id: int):
    return UserDetailResponse(
        id=user_id,
        username="alice",
        email="alice@example.com",
        is_active=True,
        full_name="Alice Johnson",
        created_at=datetime.now(),
        last_login=datetime.now(),
        profile={"bio": "Software developer", "location": "San Francisco"}
    )

🎭 响应定制

多种响应格式

python
from fastapi import Response
from fastapi.responses import JSONResponse, HTMLResponse, PlainTextResponse

@app.get("/data/{format}")
async def get_data_in_format(format: str, item_id: int = 1):
    data = {"id": item_id, "name": "Sample Item", "value": 42}
    
    if format == "json":
        return JSONResponse(content=data)
    elif format == "html":
        html_content = f"""
        <html>
            <body>
                <h1>Item {data['id']}</h1>
                <p>Name: {data['name']}</p>
                <p>Value: {data['value']}</p>
            </body>
        </html>
        """
        return HTMLResponse(content=html_content)
    elif format == "text":
        text_content = f"ID: {data['id']}, Name: {data['name']}, Value: {data['value']}"
        return PlainTextResponse(content=text_content)
    else:
        raise HTTPException(
            status_code=400,
            detail="不支持的格式,支持的格式: json, html, text"
        )

条件响应

python
from fastapi.responses import RedirectResponse

@app.get("/items/{item_id}")
async def get_item_conditional(
    item_id: int,
    format: str = "json",
    redirect_if_inactive: bool = False
):
    # 模拟物品数据
    item = {
        "id": item_id,
        "name": "Sample Item",
        "is_active": item_id % 2 == 0,  # 偶数ID为活跃
        "created_at": datetime.now().isoformat()
    }
    
    # 条件重定向
    if not item["is_active"] and redirect_if_inactive:
        return RedirectResponse(
            url=f"/items/active?suggested_id={item_id + 1}",
            status_code=status.HTTP_307_TEMPORARY_REDIRECT
        )
    
    # 条件响应格式
    if format == "summary":
        return {"id": item["id"], "name": item["name"]}
    elif format == "full":
        return item
    else:
        # 默认格式
        return {"id": item["id"], "active": item["is_active"]}

📋 状态码管理

自定义状态码

python
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item_with_status(item: Item):
    return {"message": "物品创建成功", "item": item.dict()}

@app.put("/items/{item_id}")
async def update_item_with_conditional_status(item_id: int, item: Item):
    # 模拟检查物品是否存在
    item_exists = item_id <= 100
    
    if not item_exists:
        # 创建新物品
        return JSONResponse(
            status_code=status.HTTP_201_CREATED,
            content={
                "message": "物品不存在,已创建新物品",
                "item_id": item_id,
                "item": item.dict()
            }
        )
    else:
        # 更新现有物品
        return JSONResponse(
            status_code=status.HTTP_200_OK,
            content={
                "message": "物品更新成功",
                "item_id": item_id,
                "item": item.dict()
            }
        )

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
    # 删除成功,返回204 No Content
    return Response(status_code=status.HTTP_204_NO_CONTENT)

错误响应处理

python
class ValidationErrorResponse(BaseModel):
    error_type: str = "validation_error"
    message: str
    field_errors: List[dict]

class NotFoundErrorResponse(BaseModel):
    error_type: str = "not_found"
    message: str
    resource: str

@app.get("/items/{item_id}")
async def get_item_with_error_handling(item_id: int):
    if item_id <= 0:
        return JSONResponse(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            content=ValidationErrorResponse(
                message="物品ID验证失败",
                field_errors=[{
                    "field": "item_id",
                    "error": "必须大于0",
                    "provided_value": item_id
                }]
            ).dict()
        )
    
    if item_id == 404:
        return JSONResponse(
            status_code=status.HTTP_404_NOT_FOUND,
            content=NotFoundErrorResponse(
                message="物品未找到",
                resource="item"
            ).dict()
        )
    
    return {"item_id": item_id, "name": "Sample Item"}

🔧 请求头和响应头

处理请求头

python
from fastapi import Header, Request

@app.get("/info/")
async def get_request_info(
    request: Request,
    user_agent: Optional[str] = Header(None),
    accept_language: Optional[str] = Header(None, alias="accept-language"),
    x_api_key: Optional[str] = Header(None, alias="x-api-key"),
    authorization: Optional[str] = Header(None)
):
    return {
        "client_ip": request.client.host,
        "user_agent": user_agent,
        "accept_language": accept_language,
        "has_api_key": x_api_key is not None,
        "has_auth": authorization is not None,
        "all_headers": dict(request.headers)
    }

@app.post("/upload/")
async def upload_file(
    content_type: str = Header(...),
    content_length: int = Header(...),
    x_file_name: Optional[str] = Header(None, alias="x-file-name")
):
    if content_length > 10 * 1024 * 1024:  # 10MB
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail="文件大小不能超过10MB"
        )
    
    return {
        "content_type": content_type,
        "content_length": content_length,
        "file_name": x_file_name,
        "status": "ready_to_process"
    }

设置响应头

python
from fastapi import Response

@app.get("/download/{file_id}")
async def download_file(file_id: int, response: Response):
    # 设置下载响应头
    response.headers["Content-Disposition"] = f"attachment; filename=file_{file_id}.txt"
    response.headers["Content-Type"] = "text/plain"
    response.headers["X-Download-ID"] = str(file_id)
    
    return {"content": f"这是文件 {file_id} 的内容"}

@app.get("/api/data/")
async def get_api_data():
    content = {"data": "sample", "timestamp": datetime.now().isoformat()}
    
    headers = {
        "X-API-Version": "1.0",
        "X-Rate-Limit": "100",
        "X-Rate-Remaining": "95",
        "Cache-Control": "public, max-age=300"
    }
    
    return JSONResponse(content=content, headers=headers)

🍪 Cookie处理

读取和设置Cookie

python
from fastapi import Cookie

@app.get("/profile/")
async def get_profile(
    session_id: Optional[str] = Cookie(None),
    user_preferences: Optional[str] = Cookie(None, alias="user-preferences")
):
    return {
        "has_session": session_id is not None,
        "session_id": session_id,
        "preferences": user_preferences,
        "message": "获取用户配置"
    }

@app.post("/login/")
async def login(username: str, password: str, response: Response):
    # 模拟登录验证
    if username == "admin" and password == "secret":
        # 设置会话Cookie
        response.set_cookie(
            key="session_id",
            value="abc123xyz",
            max_age=3600,  # 1小时
            httponly=True,
            secure=True,  # 仅HTTPS
            samesite="strict"
        )
        
        # 设置偏好Cookie
        response.set_cookie(
            key="user-preferences",
            value="theme=dark;lang=zh",
            max_age=86400 * 30  # 30天
        )
        
        return {"message": "登录成功", "username": username}
    else:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="用户名或密码错误"
        )

@app.post("/logout/")
async def logout(response: Response):
    # 删除Cookie
    response.delete_cookie(key="session_id")
    response.delete_cookie(key="user-preferences")
    
    return {"message": "已退出登录"}

📊 数据验证和转换

自定义验证器

python
from pydantic import BaseModel, validator, root_validator
import re

class ProductCreate(BaseModel):
    name: str = Field(..., min_length=2, max_length=100)
    sku: str = Field(..., description="产品SKU")
    price: float = Field(..., gt=0, description="产品价格")
    category: str
    specifications: dict = Field(default_factory=dict)
    
    @validator('sku')
    def validate_sku(cls, v):
        # SKU格式验证: 字母开头,后跟数字和字母
        if not re.match(r'^[A-Z]{2,3}\d{3,6}$', v.upper()):
            raise ValueError('SKU格式错误,应为2-3个字母开头加3-6个数字')
        return v.upper()
    
    @validator('category')
    def validate_category(cls, v):
        allowed_categories = ['electronics', 'clothing', 'books', 'home', 'sports']
        if v.lower() not in allowed_categories:
            raise ValueError(f'分类必须是: {", ".join(allowed_categories)}')
        return v.lower()
    
    @root_validator
    def validate_product(cls, values):
        # 跨字段验证
        category = values.get('category')
        price = values.get('price')
        
        if category == 'electronics' and price < 10:
            raise ValueError('电子产品价格不能低于10元')
        
        if category == 'books' and price > 1000:
            raise ValueError('书籍价格不能超过1000元')
        
        return values

@app.post("/products/", response_model=dict)
async def create_product(product: ProductCreate):
    return {
        "message": "产品创建成功",
        "product": product.dict(),
        "generated_id": hash(product.sku) % 10000
    }

数据转换和格式化

python
class EventCreate(BaseModel):
    title: str
    start_time: datetime
    duration_minutes: int = Field(..., ge=15, le=480)  # 15分钟到8小时
    location: str
    attendees: List[str] = Field(default_factory=list)
    
    @validator('title')
    def clean_title(cls, v):
        # 清理和格式化标题
        return ' '.join(v.strip().split())
    
    @validator('attendees', pre=True)
    def parse_attendees(cls, v):
        # 处理逗号分隔的字符串
        if isinstance(v, str):
            return [email.strip() for email in v.split(',') if email.strip()]
        return v
    
    @validator('attendees')
    def validate_emails(cls, v):
        email_pattern = r'^[^@]+@[^@]+\.[^@]+$'
        for email in v:
            if not re.match(email_pattern, email):
                raise ValueError(f'无效的邮箱地址: {email}')
        return v

@app.post("/events/")
async def create_event(event: EventCreate):
    # 计算结束时间
    end_time = event.start_time + timedelta(minutes=event.duration_minutes)
    
    return {
        "event": event.dict(),
        "computed_fields": {
            "end_time": end_time.isoformat(),
            "duration_hours": event.duration_minutes / 60,
            "attendee_count": len(event.attendees)
        }
    }

总结

本章深入介绍了FastAPI的请求和响应处理:

  • 请求体处理:Pydantic模型、嵌套模型、多参数
  • 响应模型:基础响应、模型继承、条件响应
  • 响应定制:多种格式、状态码管理
  • HTTP头处理:请求头读取、响应头设置
  • Cookie管理:读取、设置、删除Cookie
  • 数据验证:自定义验证器、数据转换

这些功能使FastAPI能够处理复杂的API需求,提供类型安全和自动文档生成的同时,保持代码的简洁性和可维护性。

请求响应设计建议

  • 使用Pydantic模型确保数据类型安全
  • 提供清晰的错误响应格式
  • 合理使用HTTP状态码
  • 添加适当的响应头信息
  • 考虑API的向后兼容性
  • 编写详细的模型文档

在下一章中,我们将学习FastAPI的表单处理和文件上传功能。

本站内容仅供学习和研究使用。