FastAPI 请求头参数(Header Parameters)完全指南:声明方式、下划线自动转换与重复头处理
本篇文章基于 FastAPI 官方教程日文版《ヘッダーのパラメータ》(即 docs/ja/docs/tutorial/header-params.md)展开,系统讲解在 FastAPI 中如何像声明查询参数、路径参数和 Cookie 一样声明 HTTP 请求头参数,并深入解析其底层实现:包括 Header 的导入与声明、变量名下划线到连字符的自动转换机制、convert_underscores 开关的使用场景与风险,以及如何用类型注解接收同一名称出现多次的重复请求头。读完本文,你将能独立实现诸如读取 User-Agent、自定义 X-Token 等真实请求头需求,并理解其背后的 FastAPI 源码行为。
为什么需要专门的 Header
HTTP 请求中经常需要读取请求头(header),例如客户端类型 User-Agent、认证令牌 X-Token、内容协商相关的 Accept-Language 等。FastAPI 提供了一种与 Query、Path、Cookie 完全对称 的声明方式:请求头参数同样通过在函数签名中做类型标注来声明,并自动获得类型校验、OpenAPI 文档生成、交互式文档展示等能力。
需要特别强调的是:声明请求头必须使用 Header。教程在"note"提示中明确指出,如果不用 Header 而仅做普通类型标注,FastAPI 会把该参数解释为查询参数(query parameter),而不是请求头。这一点从源码也可以得到印证:在 fastapi/dependencies/utils.py 中,当一个标量参数没有显式指定 FieldInfo(如 Query、Path 等)时,会被默认归类到 params.Query,从而进入查询参数通道。
导入 Header
从 fastapi 中导入 Header 与导入其他参数类型没有区别,参考示例源码 docs_src/header_params/tutorial001_an_py310.py:
from typing import Annotated
from fastapi import FastAPI, Header
app = FastAPI()
声明请求头参数
方式一:使用 Annotated(推荐,本仓库示例采用的写法)
@app.get("/items/")
async def read_items(user_agent: Annotated[str | None, Header()] = None):
return {"User-Agent": user_agent}
- 类型标注为
str | None,默认值None,表示该请求头可选;客户端不发送时user_agent即为None; Header()告诉 FastAPI 这是从 HTTP 请求头中取值;- 除了默认值,
Header()内还可以传入与Query/Path/Cookie相同的额外校验参数与注解参数,例如title、description、min_length、max_length、pattern、gt、ge、lt、le、examples等,声明方式完全一致。
方式二:使用默认值语法
对于不使用 Annotated 的 Python 版本写法,教程对应的非 Annotated 版本见 docs_src/header_params/tutorial001_py310.py:
@app.get("/items/")
async def read_items(user_agent: str | None = Header(default=None)):
return {"User-Agent": user_agent}
两种写法语义等价。仓库中的单元测试 tests/test_tutorial/test_header_params/test_tutorial001.py 会同时加载 tutorial001_py310 与 tutorial001_an_py310 两个模块分别验证。
技术细节:Header 到底是什么
教程的"技术细节"说明框指出:
Header是Path、Query、Cookie的"姊妹类",它们共同继承自同一个公共基类Param;- 但请注意:从
fastapi导入的Query、Path、Header等在形式上看起来像类,实际上返回特殊类的函数。
这一点可以在源码中得到完整验证:
- fastapi/param_functions.py 中
def Header(...)是一个函数(含# noqa: N802说明其刻意使用大写命名),它收集全部关键字参数后最终实例化并返回params.Header(...); - fastapi/params.py 中定义了真正的
class Header(Param),并在类体内通过in_ = ParamTypes.header标记该参数取自请求头。
自动转换:下划线 _ 到连字符 -
Header 相比 Path、Query、Cookie 多出了一项能力:自动转换。
HTTP 标准请求头的名字大多用连字符(hyphen,即减号 -)分隔,例如 User-Agent、X-Token。但在 Python 里,user-agent 这种写法是非法标识符,无法直接用作函数参数名。
因此,默认情况下 Header 会把你声明的 Python 参数名中的下划线 _ 自动转换为连字符 -,再用转换后的名字去实际请求头中取值,并以此名字生成 OpenAPI 文档。
同时,由于 HTTP 请求头本身不区分大小写,你可以完全按 Python 的 snake_case 风格书写参数名。也就是说,读取 User-Agent 时不必写成 User_Agent 这种别扭形式,直接写:
async def read_items(user_agent: Annotated[str | None, Header()] = None):
...
FastAPI 会帮你把 user_agent 匹配到实际的 User-Agent 请求头。
源码层面的转换实现
这一转换在 FastAPI 依赖解析时完成,见 fastapi/dependencies/utils.py:
if not field_info.alias and getattr(field_info, "convert_underscores", None):
alias = param_name.replace("_", "-")
else:
alias = field_info.alias or param_name
field_info.alias = alias
逻辑要点:
- 只有当参数名没有被显式
alias覆盖、且convert_underscores开启时,才执行param_name.replace("_", "-"); - 转换后的字符串被写回
field_info.alias,后续请求头取值与 OpenAPI 生成都以该 alias 为准。
从生成的 OpenAPI Schema 可以直观看到转换结果。测试 tests/test_tutorial/test_header_params/test_tutorial001.py 中的 test_openapi_schema 断言了 /openapi.json 中参数对象为:
{
"required": false,
"schema": {"anyOf": [{"type": "string"}, {"type": "null"}]},
"name": "user-agent",
"in": "header"
}
即:Python 参数 user_agent 在文档中呈现为请求头 user-agent(header 请求头名大小写不敏感,文档中用小写展示是惯例)。
用测试验证运行行为
test_tutorial001.py 的请求级测试也验证了实际取值逻辑:
("/items", None, 200, {"User-Agent": "testclient"}),
("/items", {"X-Header": "notvalid"}, 200, {"User-Agent": "testclient"}),
("/items", {"User-Agent": "FastAPI test"}, 200, {"User-Agent": "FastAPI test"}),
- 不发送任何请求头时,默认
user_agent为None……(在 TestClient 环境下,默认请求会携带User-Agent: testclient,因此无参请求也能取到该默认值); - 发送
User-Agent: FastAPI test时,user_agent准确取到字符串FastAPI test; - 发送无关请求头
X-Header不影响取值。
关闭自动转换:convert_underscores=False
绝大多数场景下你应该保持自动转换开启。但假如出于某种原因,你需要请求头中保留下划线(例如某些内部或历史系统的自定义头就叫 X_Strange_Header),可以给 Header 传入 convert_underscores=False:
示例见 docs_src/header_params/tutorial002_an_py310.py:
@app.get("/items/")
async def read_items(
strange_header: Annotated[str | None, Header(convert_underscores=False)] = None,
):
return {"strange_header": strange_header}
这样声明后,FastAPI 不会把 strange_header 中的下划线转为连字符,而是直接用原名去请求头中匹配名为 strange_header 的头(由于 HTTP 头大小写不敏感,Strange_Header 等大小写写法也可命中)。
该参数在源码中的默认值是 True。在 fastapi/param_functions.py 的函数签名与 fastapi/params.py 的类构造器中均可看到:
convert_underscores: bool = True,
同时,fastapi/openapi/utils.py 在生成 OpenAPI 时也会读取该标记,用于决定文档中 header 名称应保持下划线形式还是转换为连字符形式,保证"文档与实际请求头名称"一致。
官方警告:使用下划线请求头的风险
教程特别给出了 warning:在把 convert_underscores 设为 False 之前,务必意识到某些 HTTP 代理(proxy)和服务器不允许使用包含下划线的请求头。 也就是说,即使你的应用代码这样声明了,实际部署环境中若中间经过这类代理/服务器,带下划线的头可能在到达应用前就被丢弃或拒绝。因此,是否关闭自动转换需要在真实部署环境中谨慎评估,一般建议优先使用标准的连字符命名。
接收重复的请求头:类型声明为 list
HTTP 规范允许同一个请求头在报文中出现多次,即同一名称携带多个值。例如:
X-Token: foo
X-Token: bar
要接收全部值,只需在类型声明中把参数类型写成 list(并搭配可空标注使该头可选)。示例见 docs_src/header_params/tutorial003_an_py310.py:
@app.get("/items/")
async def read_items(x_token: Annotated[list[str] | None, Header()] = None):
return {"X-Token values": x_token}
此时 FastAPI 会把重复请求头的所有值作为一个 Python list 传入。对于上面的两条 X-Token,接口返回:
{
"X-Token values": [
"bar",
"foo"
]
}
测试对重复头的验证
tests/test_tutorial/test_header_params/test_tutorial003.py 使用元组列表的方式模拟重复头并断言结果:
("/items", None, 200, {"X-Token values": None}),
("/items", {"x-token": "foo"}, 200, {"X-Token values": ["foo"]}),
("/items", [("x-token", "foo"), ("x-token", "bar")], 200, {"X-Token values": ["foo", "bar"]}),
可以看到:
- 未发送该头时返回
None; - 发送单个
x-token: foo时返回单元素列表["foo"]; - 发送两条时完整返回
["foo", "bar"]。
同时 test_openapi_schema 断言了该参数在 OpenAPI 中的 schema 类型为数组:
{
"name": "x-token",
"in": "header",
"schema": {
"title": "X-Token",
"anyOf": [
{"type": "array", "items": {"type": "string"}},
{"type": "null"}
]
}
}
即 Python 类型 list[str] | None 被正确映射为可空字符串数组,Swagger UI(/docs)中会据此展示该参数可多次添加。
运行与验证方式
教程对应的可运行示例全部位于 docs_src/header_params/ 目录:
| 示例文件 | 讲解内容 |
|---|---|
| tutorial001_an_py310.py / tutorial001_py310.py | 基础请求头声明(读取 User-Agent),Annotated 与默认值两种写法 |
| tutorial002_an_py310.py / tutorial002_py310.py | convert_underscores=False 关闭自动转换 |
| tutorial003_an_py310.py / tutorial003_py310.py | 以 list 接收重复请求头 X-Token |
需要本机验证时,可任选其中一个文件(例如 tutorial001_an_py310.py),用 Uvicorn 启动:
uvicorn docs_src.header_params.tutorial001_an_py310:app --reload
随后访问 http://127.0.0.1:8000/docs 在 Swagger UI 中填写请求头并发送,或直接访问 http://127.0.0.1:8000/items/ 观察 User-Agent 回显。仓库中 _an_py310 后缀表示该版本基于 Python 3.10+ 的 typing.Annotated 语法;若运行环境版本较低,可对照阅读其非 Annotated 版本。
小结
- 请求头参数使用
Header声明,其声明模式与Query、Path、Cookie完全相同,并共享同一套默认值、校验与注解参数能力; - 请求头必须使用
Header,否则会被当作查询参数处理; - 默认情况下,FastAPI 自动把参数名中的下划线
_转为连字符-,且不区分大小写,因此可以直接用user_agent匹配User-Agent; - 若需要保留带下划线的请求头,设置
Header(convert_underscores=False),但需先确认部署链路上的代理与服务器允许这类头; - 同一请求头可能出现多次,把参数类型声明为
list即可接收其全部值。
从 Header 的类定义(fastapi/params.py)、参数收集函数(fastapi/param_functions.py)到依赖解析中的 alias 转换(fastapi/dependencies/utils.py)以及 OpenAPI 生成逻辑(fastapi/openapi/utils.py),整个请求头处理链路环环相扣——声明请求头时你不必操心变量名中的下划线,FastAPI 会为你完成全部转换。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00