1. 来源
一个 HTTP 请求可以从多个位置携带数据。FastAPI 会根据路径模板、参数类型和显式标记,判断一个函数参数来自哪里。
| 来源 | 典型用途 | FastAPI 写法 |
|---|---|---|
| Path | 定位具体资源 | 路径中的 {name} |
| Query | 搜索、分页、开关 | 简单类型参数或 Query() |
| Body | 提交结构化数据 | Pydantic 模型或 Body() |
| Header | 认证、追踪、协商 | Header() |
| Cookie | 浏览器会话标识 | Cookie() |
| Form | HTML 表单 | Form() |
| File | 上传文件 | UploadFile、File() |
路径函数的参数列表就是接口声明。FastAPI 会读取它,而不是要求我们手动从原始请求字符串中解析所有内容。
2. 路由聚合
这一章的案例没有分别创建独立的 FastAPI() 应用,而是每个文件声明一个 APIRouter。src/fastapi/api/req/__init__.py 负责导入并聚合这些案例,同时为整组请求参数案例添加 /req 前缀:
01from fastapi import APIRouter0203from . import (04body,05cookies,06form,07headers,08mixed_request,09multiple_body,10multipart,11query_list,12raw_request,13upload,14)1516# /api/v1 由上层聚合路由统一添加,本模块只声明自己的领域前缀。17router = APIRouter(prefix="/req", tags=["requests"])1819router.include_router(body.router)20router.include_router(multiple_body.router)21router.include_router(mixed_request.router)22router.include_router(query_list.router)23router.include_router(headers.router)24router.include_router(cookies.router)25router.include_router(form.router)26router.include_router(upload.router)27router.include_router(multipart.router)28router.include_router(raw_request.router)
最终路径由三部分组合:上层版本前缀、请求案例前缀和案例文件中的相对路径。例如 body.py 声明 @router.post("/chat"),最终接口就是:
1/api/v1 + /req + /chat = /api/v1/req/chat
这样每个案例文件只关心自己的相对路径,版本号和领域前缀分别由聚合路由统一管理。后面的代码块均与仓库中的真实文件保持一致。
3. JSON 请求体
JSON 请求体通常使用 Pydantic 模型声明:
下面的案例会把这个 JSON 请求发送到本地 FastAPI 服务。你可以修改请求体,再观察服务端返回的校验结果:
01from fastapi import APIRouter02from pydantic import BaseModel0304router = APIRouter()050607class ChatRequest(BaseModel):08question: str09conversation_id: str | None = None10stream: bool = False111213@router.post("/chat")14async def chat(request: ChatRequest) -> dict[str, str | bool | None]:15return {16"question": request.question,17"conversation_id": request.conversation_id,18"stream": request.stream,19}
客户端发送:
1{2"question": "什么是 FastAPI?",3"conversation_id": "conversation-1",4"stream": false5}
FastAPI 会读取 Content-Type: application/json 的请求体,把 JSON 解析成 Python 数据,再交给 ChatRequest 校验。路径函数拿到的 request 已经是模型实例,不是未经处理的字符串。
JSON 的最外层结构必须与模型一致。上面的接口期待 question 直接位于根对象中,而不是放在 request 字段里。字段名、嵌套层级和可空规则都属于请求契约。
GET 请求虽然在技术上可能携带请求体,但相关标准没有为它定义通用语义,代理和客户端也未必支持。读取数据应使用路径参数或查询参数;需要提交复杂结构时通常使用 POST。
4. Body
一个 HTTP 请求只有一个请求体,但请求体中可以包含多个字段或对象。FastAPI 允许在函数中声明多个 Body 参数,并自动把参数名作为 JSON 的键:
这个案例对应多个 Body 参数的写法,message、options 和 trace 会一起组成一个 JSON 请求体:
01from typing import Annotated0203from fastapi import APIRouter, Body04from pydantic import BaseModel0506router = APIRouter()070809class MessageInput(BaseModel):10content: str111213class ModelOptions(BaseModel):14temperature: float = 0.7151617@router.post("/messages")18async def create_message(19message: MessageInput,20options: ModelOptions,21trace: Annotated[bool, Body()] = False,22) -> dict[str, object]:23return {24"message": message,25"options": options,26"trace": trace,27}
对应的 JSON 是:
1{2"message": { "content": "你好" },3"options": { "temperature": 0.3 },4"trace": true5}
只有一个模型参数时,FastAPI 默认直接读取根对象。如果协议要求 { "message": { ... } } 这种包裹结构,可以声明 message: Annotated[MessageInput, Body(embed=True)]。是否嵌套应由接口协议决定,不能只看 Python 参数名猜测。
5. 混合
同一个接口可以同时使用路径、查询和请求体:
下面的案例会同时提交路径参数 conversation_id、查询参数 dry_run 和 JSON Body:
01from typing import Annotated0203from fastapi import APIRouter, Query04from pydantic import BaseModel0506router = APIRouter()070809class MessageInput(BaseModel):10content: str111213@router.post("/conversations/{conversation_id}/messages")14async def create_conversation_message(15conversation_id: str,16message: MessageInput,17dry_run: Annotated[bool, Query()] = False,18) -> dict[str, str | bool]:19return {20"conversation_id": conversation_id,21"content": message.content,22"dry_run": dry_run,23}
FastAPI 的判断依据是:
conversation_id出现在路径模板中,所以来自 Path;message是 Pydantic 模型,所以来自 JSON Body;dry_run是简单类型并使用Query(),所以来自 Query。
6. 多值查询
同一个查询参数可以出现多次,例如:
1GET /api/v1/req/conversations?tag=python&tag=fastapi
这类数据要显式声明为 Query 列表:
下面的案例会把输入的标签转换成重复出现的 tag 查询参数,例如 tag=python&tag=fastapi:
01from typing import Annotated0203from fastapi import APIRouter, Query0405router = APIRouter()060708@router.get("/conversations")09async def list_conversations(10tag: Annotated[list[str] | None, Query()] = None,11) -> dict[str, list[str]]:12return {"tags": tag or []}
函数会收到 ['python', 'fastapi']。如果只写 tag: list[str] 而不使用 Query(),复杂类型可能被识别为请求体,所以参数来源不够明显时应显式标记。
外部字段名与 Python 命名习惯不一致时,可以使用别名,例如 Query(alias="page-size")。别名会进入 OpenAPI,客户端仍然发送 page-size,函数内部则使用合法的 Python 变量名。
7. 请求头
请求头适合传递不属于业务实体本身的请求元数据:
下面的案例会额外发送一个 X-Request-Id 请求头。浏览器自动维护的 User-Agent 也会由服务端一并返回:
01from typing import Annotated0203from fastapi import APIRouter, Header0405router = APIRouter()060708@router.get("/request-info")09async def request_info(10user_agent: Annotated[str | None, Header()] = None,11x_request_id: Annotated[str | None, Header()] = None,12) -> dict[str, str | None]:13return {14"user_agent": user_agent,15"request_id": x_request_id,16}
FastAPI 默认会把参数名中的下划线转换为请求头中的连字符。因此 user_agent 对应 User-Agent,x_request_id 对应 X-Request-Id。HTTP 请求头名称不区分大小写。
模型 API Key、请求追踪 ID 和内容协商通常放在 Header 中,不要混入聊天消息正文。
8. Cookie
Cookie 可以通过 Cookie() 读取:
下面的案例会先写入一个 session_id Cookie,再以带凭证的方式请求本地服务:
01from typing import Annotated0203from fastapi import APIRouter, Cookie0405router = APIRouter()060708@router.get("/session")09async def get_session(10session_id: Annotated[str | None, Cookie()] = None,11) -> dict[str, str | None]:12return {"session_id": session_id}
Cookie 由浏览器按照域名、路径、过期时间、SameSite 和 Secure 等规则管理。读取到一个 Cookie 不代表用户已经通过认证,服务器仍然需要验证它的签名、会话状态或对应凭证。
9. 表单
传统 HTML 表单通常发送 application/x-www-form-urlencoded 或 multipart/form-data,不能把它当作 JSON 请求体解析:
下面的案例使用 application/x-www-form-urlencoded 提交登录字段:
01from typing import Annotated0203from fastapi import APIRouter, Form0405router = APIRouter()060708@router.post("/login")09async def login(10username: Annotated[str, Form()],11password: Annotated[str, Form()],12) -> dict[str, str]:13# 示例只返回用户名,避免把密码写入响应或日志。14return {"username": username}
示例接收密码是为了演示表单解析,但不会把密码写入响应或日志。如果项目使用 fastapi[standard],处理表单所需的常用依赖通常已经安装。只安装最小版 FastAPI 时,需要确认项目包含 python-multipart。
10. 文件
上传文件时优先使用 UploadFile:
下面的案例使用 FormData 上传文件,对应 UploadFile 接收的 multipart/form-data 请求:
01from typing import Annotated0203from fastapi import APIRouter, File, UploadFile0405router = APIRouter()060708@router.post("/documents")09async def upload_document(10file: Annotated[UploadFile, File()],11) -> dict[str, str | int | None]:12try:13content = await file.read()14return {15"filename": file.filename,16"content_type": file.content_type,17"size": len(content),18}19finally:20await file.close()
UploadFile 提供文件名、内容类型和异步读写方法,并使用临时文件机制处理较大内容。直接声明 bytes 会把完整文件读入内存,更适合明确很小的文件。
示例为了展示 API 一次性调用了 read(),这仍会把文件内容读进内存。finally 会保证请求处理结束前关闭文件。真正的大文件应分块读取和处理;如果把文件交给其他库,也可以使用 file.file 访问底层文件对象。
不要只相信客户端提供的文件名和 content_type。生产场景还需要限制大小、检查真实格式、生成安全文件名,并决定文件保存在哪里。
11. 多部分请求
文件和普通表单字段可以放在同一个 multipart 请求中:
下面的案例会把标题和文件放入同一个 FormData,对应服务端的 Form() 与 File() 参数:
01from typing import Annotated0203from fastapi import APIRouter, File, Form, UploadFile0405router = APIRouter()060708@router.post("/knowledge/files")09async def upload_knowledge(10title: Annotated[str, Form()],11file: Annotated[UploadFile, File()],12) -> dict[str, str | None]:13try:14return {15"title": title,16"filename": file.filename,17}18finally:19await file.close()
同一个请求采用 multipart 后,不能再同时要求客户端把另一个部分作为普通的 application/json 请求体发送。复杂元数据可以拆成表单字段、编码为 JSON 字符串后显式解析,或者先创建资源再单独上传文件。这个案例同样在 finally 中关闭上传文件。
12. 媒体类型
Content-Type 告诉服务器如何解释请求体,常见值包括:
Content-Type | 典型内容 |
|---|---|
application/json | JSON 对象或数组 |
application/x-www-form-urlencoded | 普通表单字段 |
multipart/form-data | 文件与表单字段 |
text/plain | 纯文本 |
内容本身看起来像 JSON,不代表服务器应该把它当 JSON。客户端要同时发送正确的数据和媒体类型。浏览器使用 FormData 时会自动生成带 boundary 的 multipart/form-data,不要手动覆盖这个请求头,否则 boundary 丢失后服务端无法拆分各部分。
13. 原始请求
确实需要访问客户端地址、URL 或底层请求信息时,可以注入 Request:
最后用一个案例直接查看 Request 暴露的底层信息,帮助你区分声明式参数解析和原始请求对象:
01from fastapi import APIRouter, Request0203router = APIRouter()040506@router.get("/debug")07async def debug(request: Request) -> dict[str, str | None]:08return {09"method": request.method,10"path": request.url.path,11"client": request.client.host if request.client else None,12}
直接使用 Request 会绕过一部分声明式解析与文档能力。能用 Path、Query、Pydantic、Header 或依赖表达的数据,应优先使用这些高层接口。
也不要自行读取一次请求流后,又期待 FastAPI 按普通方式重复解析同一份流数据。请求体和上传文件应明确由哪一层负责读取。
14. 边界
设计聊天请求时,可以把数据按职责分开:
| 数据 | 合适位置 |
|---|---|
| 会话 ID | Path 或 Body,取决于资源设计 |
| 用户消息 | JSON Body |
| 是否流式输出 | 路由区分或 Body |
| API Key | Header |
| Request ID | Header |
| 检索文件 | multipart File |
| 分页数量 | Query |
不要把所有内容都塞进一个万能字典,也不要因为 FastAPI 可以读取原始请求,就放弃清晰的数据模型。声明式接口能同时服务于校验、编辑器提示、测试和文档。