创建时间: 2026-08-25最后更新: 2026-08-27

1. 来源

一个 HTTP 请求可以从多个位置携带数据。FastAPI 会根据路径模板、参数类型和显式标记,判断一个函数参数来自哪里。

来源典型用途FastAPI 写法
Path定位具体资源路径中的 {name}
Query搜索、分页、开关简单类型参数或 Query()
Body提交结构化数据Pydantic 模型或 Body()
Header认证、追踪、协商Header()
Cookie浏览器会话标识Cookie()
FormHTML 表单Form()
File上传文件UploadFile、File()

路径函数的参数列表就是接口声明。FastAPI 会读取它,而不是要求我们手动从原始请求字符串中解析所有内容。

2. 路由聚合

这一章的案例没有分别创建独立的 FastAPI() 应用,而是每个文件声明一个 APIRouter。src/fastapi/api/req/__init__.py 负责导入并聚合这些案例,同时为整组请求参数案例添加 /req 前缀:

api/req/__init__.py
api/router.py
01
from fastapi import APIRouter
02
03
from . import (
04
body,
05
cookies,
06
form,
07
headers,
08
mixed_request,
09
multiple_body,
10
multipart,
11
query_list,
12
raw_request,
13
upload,
14
)
15
16
# /api/v1 由上层聚合路由统一添加,本模块只声明自己的领域前缀。
17
router = APIRouter(prefix="/req", tags=["requests"])
18
19
router.include_router(body.router)
20
router.include_router(multiple_body.router)
21
router.include_router(mixed_request.router)
22
router.include_router(query_list.router)
23
router.include_router(headers.router)
24
router.include_router(cookies.router)
25
router.include_router(form.router)
26
router.include_router(upload.router)
27
router.include_router(multipart.router)
28
router.include_router(raw_request.router)

最终路径由三部分组合:上层版本前缀、请求案例前缀和案例文件中的相对路径。例如 body.py 声明 @router.post("/chat"),最终接口就是:

code.ts
1
/api/v1 + /req + /chat = /api/v1/req/chat

这样每个案例文件只关心自己的相对路径,版本号和领域前缀分别由聚合路由统一管理。后面的代码块均与仓库中的真实文件保持一致。

3. JSON 请求体

JSON 请求体通常使用 Pydantic 模型声明:

下面的案例会把这个 JSON 请求发送到本地 FastAPI 服务。你可以修改请求体,再观察服务端返回的校验结果:

预览
本地服务:心跳检测中
body.py
01
from fastapi import APIRouter
02
from pydantic import BaseModel
03
04
router = APIRouter()
05
06
07
class ChatRequest(BaseModel):
08
question: str
09
conversation_id: str | None = None
10
stream: bool = False
11
12
13
@router.post("/chat")
14
async def chat(request: ChatRequest) -> dict[str, str | bool | None]:
15
return {
16
"question": request.question,
17
"conversation_id": request.conversation_id,
18
"stream": request.stream,
19
}

客户端发送:

request.json
1
{
2
"question": "什么是 FastAPI?",
3
"conversation_id": "conversation-1",
4
"stream": false
5
}

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 请求体:

预览
本地服务:心跳检测中
multiple_body.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, Body
04
from pydantic import BaseModel
05
06
router = APIRouter()
07
08
09
class MessageInput(BaseModel):
10
content: str
11
12
13
class ModelOptions(BaseModel):
14
temperature: float = 0.7
15
16
17
@router.post("/messages")
18
async def create_message(
19
message: MessageInput,
20
options: ModelOptions,
21
trace: Annotated[bool, Body()] = False,
22
) -> dict[str, object]:
23
return {
24
"message": message,
25
"options": options,
26
"trace": trace,
27
}

对应的 JSON 是:

multiple-body.json
1
{
2
"message": { "content": "你好" },
3
"options": { "temperature": 0.3 },
4
"trace": true
5
}

只有一个模型参数时,FastAPI 默认直接读取根对象。如果协议要求 { "message": { ... } } 这种包裹结构,可以声明 message: Annotated[MessageInput, Body(embed=True)]。是否嵌套应由接口协议决定,不能只看 Python 参数名猜测。

5. 混合

同一个接口可以同时使用路径、查询和请求体:

下面的案例会同时提交路径参数 conversation_id、查询参数 dry_run 和 JSON Body:

预览
本地服务:心跳检测中
mixed_request.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, Query
04
from pydantic import BaseModel
05
06
router = APIRouter()
07
08
09
class MessageInput(BaseModel):
10
content: str
11
12
13
@router.post("/conversations/{conversation_id}/messages")
14
async def create_conversation_message(
15
conversation_id: str,
16
message: MessageInput,
17
dry_run: Annotated[bool, Query()] = False,
18
) -> dict[str, str | bool]:
19
return {
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. 多值查询

同一个查询参数可以出现多次,例如:

code.ts
1
GET /api/v1/req/conversations?tag=python&tag=fastapi

这类数据要显式声明为 Query 列表:

下面的案例会把输入的标签转换成重复出现的 tag 查询参数,例如 tag=python&tag=fastapi:

预览
本地服务:心跳检测中
query_list.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, Query
04
05
router = APIRouter()
06
07
08
@router.get("/conversations")
09
async def list_conversations(
10
tag: Annotated[list[str] | None, Query()] = None,
11
) -> dict[str, list[str]]:
12
return {"tags": tag or []}

函数会收到 ['python', 'fastapi']。如果只写 tag: list[str] 而不使用 Query(),复杂类型可能被识别为请求体,所以参数来源不够明显时应显式标记。

外部字段名与 Python 命名习惯不一致时,可以使用别名,例如 Query(alias="page-size")。别名会进入 OpenAPI,客户端仍然发送 page-size,函数内部则使用合法的 Python 变量名。

7. 请求头

请求头适合传递不属于业务实体本身的请求元数据:

下面的案例会额外发送一个 X-Request-Id 请求头。浏览器自动维护的 User-Agent 也会由服务端一并返回:

预览
本地服务:心跳检测中
headers.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, Header
04
05
router = APIRouter()
06
07
08
@router.get("/request-info")
09
async def request_info(
10
user_agent: Annotated[str | None, Header()] = None,
11
x_request_id: Annotated[str | None, Header()] = None,
12
) -> dict[str, str | None]:
13
return {
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 中,不要混入聊天消息正文。

Cookie 可以通过 Cookie() 读取:

下面的案例会先写入一个 session_id Cookie,再以带凭证的方式请求本地服务:

预览
本地服务:心跳检测中
cookies.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, Cookie
04
05
router = APIRouter()
06
07
08
@router.get("/session")
09
async def get_session(
10
session_id: Annotated[str | None, Cookie()] = None,
11
) -> dict[str, str | None]:
12
return {"session_id": session_id}

Cookie 由浏览器按照域名、路径、过期时间、SameSite 和 Secure 等规则管理。读取到一个 Cookie 不代表用户已经通过认证,服务器仍然需要验证它的签名、会话状态或对应凭证。

9. 表单

传统 HTML 表单通常发送 application/x-www-form-urlencoded 或 multipart/form-data,不能把它当作 JSON 请求体解析:

下面的案例使用 application/x-www-form-urlencoded 提交登录字段:

预览
本地服务:心跳检测中
form.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, Form
04
05
router = APIRouter()
06
07
08
@router.post("/login")
09
async def login(
10
username: Annotated[str, Form()],
11
password: Annotated[str, Form()],
12
) -> dict[str, str]:
13
# 示例只返回用户名,避免把密码写入响应或日志。
14
return {"username": username}

示例接收密码是为了演示表单解析,但不会把密码写入响应或日志。如果项目使用 fastapi[standard],处理表单所需的常用依赖通常已经安装。只安装最小版 FastAPI 时,需要确认项目包含 python-multipart。

10. 文件

上传文件时优先使用 UploadFile:

下面的案例使用 FormData 上传文件,对应 UploadFile 接收的 multipart/form-data 请求:

预览
本地服务:心跳检测中
upload.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, File, UploadFile
04
05
router = APIRouter()
06
07
08
@router.post("/documents")
09
async def upload_document(
10
file: Annotated[UploadFile, File()],
11
) -> dict[str, str | int | None]:
12
try:
13
content = await file.read()
14
return {
15
"filename": file.filename,
16
"content_type": file.content_type,
17
"size": len(content),
18
}
19
finally:
20
await file.close()

UploadFile 提供文件名、内容类型和异步读写方法,并使用临时文件机制处理较大内容。直接声明 bytes 会把完整文件读入内存,更适合明确很小的文件。

示例为了展示 API 一次性调用了 read(),这仍会把文件内容读进内存。finally 会保证请求处理结束前关闭文件。真正的大文件应分块读取和处理;如果把文件交给其他库,也可以使用 file.file 访问底层文件对象。

不要只相信客户端提供的文件名和 content_type。生产场景还需要限制大小、检查真实格式、生成安全文件名,并决定文件保存在哪里。

11. 多部分请求

文件和普通表单字段可以放在同一个 multipart 请求中:

下面的案例会把标题和文件放入同一个 FormData,对应服务端的 Form() 与 File() 参数:

预览
本地服务:心跳检测中
multipart.py
01
from typing import Annotated
02
03
from fastapi import APIRouter, File, Form, UploadFile
04
05
router = APIRouter()
06
07
08
@router.post("/knowledge/files")
09
async def upload_knowledge(
10
title: Annotated[str, Form()],
11
file: Annotated[UploadFile, File()],
12
) -> dict[str, str | None]:
13
try:
14
return {
15
"title": title,
16
"filename": file.filename,
17
}
18
finally:
19
await file.close()

同一个请求采用 multipart 后,不能再同时要求客户端把另一个部分作为普通的 application/json 请求体发送。复杂元数据可以拆成表单字段、编码为 JSON 字符串后显式解析,或者先创建资源再单独上传文件。这个案例同样在 finally 中关闭上传文件。

12. 媒体类型

Content-Type 告诉服务器如何解释请求体,常见值包括:

Content-Type典型内容
application/jsonJSON 对象或数组
application/x-www-form-urlencoded普通表单字段
multipart/form-data文件与表单字段
text/plain纯文本

内容本身看起来像 JSON,不代表服务器应该把它当 JSON。客户端要同时发送正确的数据和媒体类型。浏览器使用 FormData 时会自动生成带 boundary 的 multipart/form-data,不要手动覆盖这个请求头,否则 boundary 丢失后服务端无法拆分各部分。

13. 原始请求

确实需要访问客户端地址、URL 或底层请求信息时,可以注入 Request:

最后用一个案例直接查看 Request 暴露的底层信息,帮助你区分声明式参数解析和原始请求对象:

预览
本地服务:心跳检测中
raw_request.py
01
from fastapi import APIRouter, Request
02
03
router = APIRouter()
04
05
06
@router.get("/debug")
07
async def debug(request: Request) -> dict[str, str | None]:
08
return {
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. 边界

设计聊天请求时,可以把数据按职责分开:

数据合适位置
会话 IDPath 或 Body,取决于资源设计
用户消息JSON Body
是否流式输出路由区分或 Body
API KeyHeader
Request IDHeader
检索文件multipart File
分页数量Query

不要把所有内容都塞进一个万能字典,也不要因为 FastAPI 可以读取原始请求,就放弃清晰的数据模型。声明式接口能同时服务于校验、编辑器提示、测试和文档。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容