路径参数、查询参数、请求头与 Cookie
一个 HTTP 请求到达 FastAPI 时,会携带很多信息。本章要讲清楚其中四类“数据从哪来、怎么取、怎么校验”:
| 数据来源 | 生活化类比 | 在请求里的位置 | FastAPI 里的写法 |
|---|---|---|---|
| 路径参数 | 快递地址上的门牌号 | URL 路径中 {...} | 函数参数 + 类型标注 |
| 查询参数 | 点外卖时的“筛选条件” | URL 中 ? 之后 | 带默认值的函数参数 |
| 请求头 | 快递面单上的备注说明 | HTTP 头部 | Annotated[类型, Header()] |
| Cookie | 商家发的会员卡(贴身上) | 请求头部里的 Cookie 字段 | Annotated[类型, Cookie()] |
下面逐一展开。示例统一从下面的骨架开始:
from fastapi import FastAPI
app = FastAPI()关于
Annotated写法:从 FastAPI 0.95 起,官方推荐用 Python 的Annotated来声明带校验的参数——把“类型”和“校验规则”写在一起,代码更整洁。以查询参数为例:python# 旧写法(仍可用,但不推荐) limit: int = Query(default=10, ge=1, le=100) # 新写法(推荐) limit: Annotated[int, Query(ge=1, le=100)] = 10要点:默认值从
Query(default=...)里挪到了参数本身的= 10;必填参数则干脆不写默认值。本章后续所有需要Path/Query/Header/Cookie的示例统一用Annotated写法。
一、路径参数(Path Parameters)
1. 什么是路径参数
路径参数是“长在 URL 路径里”的那部分数据,用花括号占位。它天生表达“到底是哪一个资源”。
比如 /users/3 里的 3,就是在说“第 3 号用户”,而不是 1 号或 2 号。它像快递地址里的门牌号:北京市/朝阳区/xx小区/3号楼,最后一个数字决定了包裹送到哪一户。
2. 基本用法
在装饰器路径里用 {名字} 占位,函数里用同名参数接收即可:
@app.get("/users/{user_id}")
def read_user(user_id):
return {"user_id": user_id}访问 /users/3,返回 {"user_id": "3"}。
注意:没有写类型时,FastAPI 默认把它当字符串处理,3 是字符串 "3",不是数字 3。
3. 用类型标注做自动转换 + 校验
给参数加上类型标注,FastAPI 会自动做两件事:转换类型和校验合法性。
@app.get("/users/{user_id}")
def read_user(user_id: int):
return {"user_id": user_id}现在访问 /users/3,返回 {"user_id": 3}(真正的整数)。
而访问 /users/abc,会直接返回 422 Unprocessable Entity,并附上一段 JSON 告诉你错在哪:
{
"detail": [
{
"loc": ["path", "user_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}这就是 FastAPI 的核心卖点之一:你只写类型标注,它替你挡掉非法输入,不再需要手写 if not user_id.isdigit() 之类的判断。
4. 用枚举限制取值范围
有些路径参数的合法取值只有固定几个。用 Python 的 Enum 可以让文档和校验都更清晰:
from enum import Enum
class Color(str, Enum):
red = "red"
green = "green"
blue = "blue"
@app.get("/lights/{color}")
def set_light(color: Color):
if color is Color.red:
return {"color": color, "msg": "红灯亮"}
return {"color": color}访问 /lights/red 正常;访问 /lights/pink 会得到 422,因为 pink 不在枚举里。str 继承让枚举值能像普通字符串一样参与返回和比较。
5. 路径参数里含有斜杠的特殊情况
默认 {...} 匹配到 / 就停。如果路径参数本身可能带斜杠(比如文件路径 images/a/b.png),需要写 {参数名:path}:
@app.get("/files/{file_path:path}")
def read_file(file_path: str):
return {"file_path": file_path}访问 /files/images/a/b.png,file_path 得到 images/a/b.png。这是 Starlette 提供的路径转换器语法。
6. 用 Path 给路径参数加约束
除了类型,还能用 Path 加更多校验规则,比如数值范围、字符串长度:
from typing import Annotated
from fastapi import Path
@app.get("/items/{item_id}")
def read_item(
item_id: Annotated[int, Path(title="商品ID", ge=1, le=1000)],
):
return {"item_id": item_id}Annotated[int, Path(...)] 里的 int 声明类型,Path(...) 附加校验规则。不写默认值时,参数默认就是必填。
ge=1:大于等于 1(greater than or equal)。le=1000:小于等于 1000。title:会在自动生成的/docs文档里显示为字段说明。
常用约束一览:
| 参数 | 含义 | 适用类型 |
|---|---|---|
ge / gt | 大于等于 / 大于 | 数字 |
le / lt | 小于等于 / 小于 | 数字 |
min_length | 最小长度 | 字符串 |
max_length | 最大长度 | 字符串 |
pattern | 正则匹配 | 字符串 |
title | 文档中的字段名 | 任意 |
description | 文档中的字段说明 | 任意 |
7. 固定路径放在动态路径之前
这一条在《路由与 HTTP 方法》一章已讲过,这里再强调一次,因为它是路径参数最容易踩的坑:
# 正确:具体的固定路径写在前面
@app.get("/users/me")
def current_user():
return {"name": "当前用户"}
@app.get("/users/{user_id}")
def read_user(user_id: int):
return {"user_id": user_id}如果顺序反了,/users/me 会先被 /users/{user_id} 截获,再因为 me 不是整数而报 422。官方路径参数说明
二、查询参数(Query Parameters)
1. 什么是查询参数
查询参数是 URL 里 ? 后面的那串 key=value,多个之间用 & 连接:
https://shop.com/items?skip=0&limit=20&keyword=手机它表达“对同一批资源做筛选、分页、排序”等附加条件,像点外卖时选的“辣度、份量、不要香菜”——不改变“点的是哪个菜”,只改变“这道菜怎么做”。
和路径参数的区别很直观:
| 对比项 | 路径参数 /items/{id} | 查询参数 /items?q=手机 |
|---|---|---|
| 是否必填 | 通常必填 | 通常可选(可给默认值) |
| 定位对象 | 某个具体资源 | 一批资源的筛选/分页 |
| 位置 | 路径中间 | ? 之后 |
2. 基本用法:只要不是路径参数,剩下的默认都是查询参数
@app.get("/items/")
def read_items(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}FastAPI 的规则很简单:函数参数名如果没出现在路径里,也没有被 Body 之类标记,它就是查询参数。
- 访问
/items/:用默认值,返回{"skip": 0, "limit": 10}。 - 访问
/items/?skip=5&limit=3:返回{"skip": 5, "limit": 3}。 - 访问
/items/?limit=20:skip用默认值,limit用 20。
3. 可选查询参数
有些查询参数允许“不传”。用 None 作默认值即可:
@app.get("/items/")
def read_items(q: str | None = None):
if q:
return {"q": q, "msg": "搜索模式"}
return {"q": None, "msg": "列表模式"}str | None 表示“可以是字符串,也可以是空”。Python 3.9 及以下用 Optional[str] 或 Union[str, None]。
4. 必填查询参数
不加默认值就是必填:
@app.get("/items/")
def read_items(limit: int):
return {"limit": limit}访问 /items/ 不传 limit 会得到 422 校验错误。一般来说,能提供合理默认值的查询参数就别设成必填,对调用方更友好。
5. 用 Query 加校验和说明
查询参数同样支持校验,用 Query:
from typing import Annotated
from fastapi import Query
@app.get("/items/")
def read_items(
q: Annotated[str | None, Query(min_length=3, max_length=20)] = None,
limit: Annotated[int, Query(ge=1, le=100)] = 10,
):
return {"q": q, "limit": limit}min_length=3:q只要传了,就必须至少 3 个字符。ge=1、le=100:limit必须在 1~100 之间,超出返回 422。q的默认值是= None,表示可省略;limit的默认值是= 10。
Query 的约束参数和上一节的 Path 是同一套(ge、le、min_length、pattern 等)。
6. 用 alias 支持带特殊字符的参数名
Python 变量名不能带 -,但某些接口约定查询参数就是 user-name。用 alias 打通两者:
@app.get("/items/")
def read_items(user_name: Annotated[str | None, Query(alias="user-name")] = None):
return {"user_name": user_name}这样 URL 里传 /items/?user-name=张三,函数里的 user_name 能拿到值。alias 同样适用于 Path。
7. 接收多个值(列表参数)
有时同一个参数要传多次,比如多选筛选 ?tag=手机&tag=数码:
@app.get("/items/")
def read_items(tag: Annotated[list[str], Query()] = []):
return {"tag": tag}- 传一次:
?tag=手机→["手机"]。 - 传多次:
?tag=手机&tag=数码→["手机", "数码"]。
注意 list[str] 写法需要 Python 3.9+;更低版本用 from typing import List 后写 List[str]。
8. 布尔值查询参数的小心机
FastAPI 会自动把查询参数里的 1/0、true/false、yes/no、on/off 识别成布尔值:
@app.get("/items/")
def read_items(in_stock: bool = False):
return {"in_stock": in_stock}?in_stock=true、?in_stock=1、?in_stock=yes、?in_stock=on 都会得到 True。如果传了个它不认识的字符串(比如 ?in_stock=maybe),会返回 422。
三、请求头(Request Headers)
1. 什么是请求头
请求头是 HTTP 请求里“键值对”形式的元信息,描述请求本身,而不是业务数据。它像快递面单上除了收件地址之外的备注:易碎品、到付、本人签收——不影响包裹寄到哪,但影响对方怎么处理它。
常见请求头:
| 头部 | 常见内容 | 用途 |
|---|---|---|
User-Agent | 浏览器/客户端标识 | 识别来访客户端 |
Authorization | Bearer <token> 等 | 身份认证 |
Content-Type | application/json 等 | 说明请求体格式 |
Accept | application/json 等 | 期望的响应格式 |
X-Request-Id | 自定义追踪 ID | 链路追踪、日志关联 |
2. 用 Header 读取请求头
from typing import Annotated
from fastapi import Header
@app.get("/whoami")
def whoami(user_agent: Annotated[str | None, Header()] = None):
return {"User-Agent": user_agent}请求里的 User-Agent 头就能被读出来。用 curl 验证:
curl http://127.0.0.1:8000/whoami -H "User-Agent: MyClient/1.0"返回:
{ "User-Agent": "MyClient/1.0" }3. 大小写不敏感
HTTP 头的名字大小写不敏感,User-Agent、user-agent、USER-AGENT 指的都是同一个头,FastAPI 会自动统一处理,放心接收。
4. 下划线 _ 与连字符 - 的自动转换
HTTP 头习惯用 -,Python 变量名却必须用 _。FastAPI 会自动把参数名里的 _ 换成 - 去找头:
@app.get("/whoami")
def whoami(x_request_id: Annotated[str | None, Header()] = None):
return {"x-request-id": x_request_id}上面的 x_request_id 对应请求头 x-request-id。
如果业务里确实有个带下划线的头(极少见),想关闭这个自动转换,加 convert_underscores=False:
x_request_id: Annotated[str | None, Header(convert_underscores=False)] = None5. Header 也能加校验
和 Path、Query 一样,Header 支持 min_length、pattern、alias 等参数:
@app.get("/whoami")
def whoami(
x_token: Annotated[str, Header(min_length=8)],
):
return {"x_token": x_token}不写默认值时,x-token 这个头必填,且至少 8 个字符,否则返回 422。
6. 读取认证信息的典型场景
很多接口在函数里直接读 Authorization 头来做鉴权(生产环境建议用 FastAPI 的依赖注入 Depends,会更干净,见后续章节):
@app.get("/profile")
def profile(authorization: Annotated[str | None, Header()] = None):
if not authorization or not authorization.startswith("Bearer "):
return {"msg": "未登录"}
token = authorization.removeprefix("Bearer ")
return {"token": token}四、Cookie
1. 什么是 Cookie
Cookie 是服务器让浏览器“记住”的一小段数据,之后浏览器每次请求都会自动带上。它像商家发的会员卡:办卡时(服务器 Set-Cookie)发到你手里,之后每次消费(请求)你把它贴身上,商家一眼就能认出你是谁。
典型用途:登录态(Session)、用户偏好、购物车标识、广告追踪 ID 等。
2. 用 Cookie 读取 Cookie
from typing import Annotated
from fastapi import Cookie
@app.get("/items/")
def read_items(ads_id: Annotated[str | None, Cookie()] = None):
return {"ads_id": ads_id}访问时带上 Cookie,就能读到:
curl http://127.0.0.1:8000/items/ -H "Cookie: ads_id=abc123"返回:
{ "ads_id": "abc123" }Cookie 的校验参数和 Header、Query 也是一套。
3. 用 Response 设置 Cookie
读取用 Cookie,写入则通过 Response 对象:
from fastapi import Response
@app.post("/login")
def login(response: Response):
# 模拟登录成功,种下一个 session cookie
response.set_cookie(
key="session",
value="abc123",
httponly=True, # 禁止 JS 读取,降低 XSS 风险
max_age=3600, # 有效期 1 小时(秒)
samesite="lax", # 跨站请求时是否携带
)
return {"msg": "登录成功"}set_cookie 常用参数:
| 参数 | 含义 |
|---|---|
key | Cookie 名 |
value | Cookie 值 |
max_age | 有效期(秒) |
path | 生效路径,默认 / 全站生效 |
httponly | True 时 JS 读不到,更安全 |
secure | True 时仅 HTTPS 传输 |
samesite | 跨站携带策略:lax / strict / none |
删除 Cookie 则是设一个已过期的 Cookie:
@app.post("/logout")
def logout(response: Response):
response.delete_cookie(key="session")
return {"msg": "已退出"}4. 读取 vs 设置的完整闭环
下面用一个极简的“登录 → 识别 → 退出”示例串起 Cookie 的读和写:
@app.post("/login")
def login(response: Response):
response.set_cookie(key="session", value="abc123", httponly=True)
return {"msg": "登录成功"}
@app.get("/whoami")
def whoami(session: Annotated[str | None, Cookie()] = None):
if session == "abc123":
return {"msg": "你是已登录用户"}
return {"msg": "匿名访客"}
@app.post("/logout")
def logout(response: Response):
response.delete_cookie(key="session")
return {"msg": "已退出"}流程:POST /login 种 Cookie → GET /whoami 浏览器自动带上 Cookie 被识别 → POST /logout 删除 Cookie。这个例子只演示机制,真实登录态需要加密和会话存储,不能这样明文判断。
五、四者对比速查表
| 维度 | 路径参数 | 查询参数 | 请求头 | Cookie |
|---|---|---|---|---|
| 位置 | URL 路径 {} | URL ? 之后 | HTTP 头部 | HTTP 头部 Cookie |
| 典型用途 | 定位具体资源 | 筛选/分页/排序 | 元信息、鉴权 | 会话、偏好、追踪 |
| 必填性 | 通常必填 | 通常可选 | 视情况 | 视情况 |
| 是否自动携带 | 手动写进 URL | 手动写进 URL | 客户端/浏览器决定 | 浏览器自动带上 |
| FastAPI 写法 | 函数参数 + 类型 | 带默认值参数 | Header(...) | Cookie(...) |
| 校验工具 | Path(...) | Query(...) | Header(...) | Cookie(...) |
六、一个综合示例
把四类数据放进一个接口里,体会它们各司其职:
from typing import Annotated
from enum import Enum
from fastapi import FastAPI, Cookie, Header, Path, Query
class Lang(str, Enum):
zh = "zh"
en = "en"
app = FastAPI()
@app.get("/shops/{shop_id}/items/")
def search_items(
shop_id: Annotated[int, Path(ge=1, title="店铺ID")],
keyword: Annotated[str | None, Query(min_length=2)] = None,
page: Annotated[int, Query(ge=1)] = 1,
lang: Annotated[Lang, Query()] = Lang.zh,
x_request_id: Annotated[str | None, Header()] = None,
session: Annotated[str | None, Cookie()] = None,
):
return {
"shop_id": shop_id, # 来自路径:定位哪家店
"keyword": keyword, # 来自查询:搜什么
"page": page, # 来自查询:第几页
"lang": lang, # 来自查询:什么语言
"x_request_id": x_request_id, # 来自请求头:追踪本次请求
"session": session, # 来自 Cookie:识别登录态
}一个可能的请求:
GET /shops/12/items/?keyword=手机&page=2&lang=zh对应的四类数据被 FastAPI 自动解析、转换、校验后,干干净净地交到函数里。
本章练习
为图书系统设计一个接口:GET /books/{book_id} 返回书籍详情,同时支持:
- 用查询参数
fields控制返回哪些字段(可选); - 用请求头
x-client记录调用方标识; - 用 Cookie
recent_view读取用户最近浏览记录。
过关标准:能说出四类参数各自的位置和适用场景;能解释为什么 fields 适合做查询参数而不是路径参数;能写出一个同时读取四类数据的接口。