Skip to content

FastAPI ​

截屏2026-09-12 14.11.31.png


FastAPI简介 ​

FastAPI 由 Sebastian Ramirez 于 2018 年 12 月开发,是一个现代 Python 网络框架,在构建 API 方面非常高效。 它基于 Python 的类型提示功能,该功能自 Python 3.6 起添加。 它是 Python 最快的 Web 框架之一。

  • 由于它基于 Starlette 和 Pydantic 库的功能,其性能名列前茅,与 NodeJS 和 Go 相当。
  • 除了提供高性能之外,FastAPI 还提供显着的开发速度、减少代码中人为引起的错误、易于学习并且完全可用于生产。
  • FastAPI 完全兼容著名的 API 标准,即 OpenAPI 和 JSON 模式。

FastAPI实例 ​

截屏2026-09-12 14.24.21.png


创建项目 ​

FastAPI → 存储位置及项目名称 → 创建虚拟环境 → Create

截屏2026-09-12 14.15.31.png

为什么要创建虚拟环境?

隔离项目运行环境,避免依赖冲突,保持全局环境的干净和稳定


FastAPI – 环境设置 ​

要安装 FastAPI(最好在虚拟环境中),请使用 pip 安装程序。

bash
pip3 install fastapi

FastAPI 依赖于 Starlette 和 Pydantic 库,因此它们也会被安装。


安装 Uvicorn ​

FastAPI 不附带任何内置服务器应用程序。 要运行 FastAPI 应用程序,您需要一个名为 uvicorn 的 ASGI 异步服务器接口,所以也使用 pip 安装程序安装它。 它还将安装 uvicorn 的依赖项——asgiref、click、h11 和 typing-extension

bash
pip3 install uvicorn

创建 FastAPI 应用程序 ​

  1. 声明 FastAPI 类的应用程序对象。
python
from fastapi import FastAPI
app = FastAPI()
  1. 创建路由——路由就是URL 地址和处理函数之间的映射关系,它决定了当用户访问某个特定网址时,服务器应该执行哪段代码来返回结果。
python
@app.get("/")
async def root():
   return {"message": "Hello World"}

截屏2026-09-12 14.36.26.png


运行项目 ​

代码运行 ​

python
app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

# 启动FastAPI应用
if __name__ == "__main__":
    import uvicorn

    uvicorn.run(
        "main:app",      # 格式为「文件名:FastAPI实例名」,这样 reload 才能生效
        host="127.0.0.1",  # 只监听本机;需要局域网/手机访问可改为 0.0.0.0
        port=8000,
        reload=True,     # 代码改动后自动重启,仅开发时使用
    )

命令行运行(建议) ​

bash
uvicorn main:app --reload

这是 FastAPI / Starlette 框架最常用的启动命令,作用是启动一个基于 Uvicorn 的 ASGI 服务器,运行你的 Python Web 应用。

  1. uvicorn

    轻量、高性能的 Python ASGI 异步服务器接口

  2. main:app

    模块名:应用实例 • main → 你的 Python 文件名 main.py • app → 你在代码里创建的 FastAPI 实例对象(通常写 app = FastAPI())

  3. —reload • 作用:开发模式自动重载 • 效果:你修改代码后保存即生效,不用手动重启服务器 • 注意:只在开发时用,生产环境不要加


通过命令启动 uvicorn 服务器。 ​

bash
uvicorn main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.

浏览器访问http://localhost/8000。 将在浏览器窗口中看到返回结果。

截屏2026-09-12 15.19.47.png

通过访问http://127.0.0.1:8000/docs得到FastAPI交互式文档

截屏2026-09-12 15.24.02.png

FastAPI 还支持 Redoc ( https://github.com/Redocly/redoc) 提供的另一种自动文档化方法。

截屏2026-09-12 17.25.37.png


FastAPI - 参数 ​

参数就是客户端发送请求时附带的额外信息和指令

参数的作用是让同一个接口能根据不同的输入,返回不同的输出,实现动态交互

截屏2026-09-12 17.40.27.png


参数分类 ​

截屏2026-09-12 17.43.03.png


FastAPI参数类型判断流程 ​

截屏2026-09-13 00.05.51.png


路径参数 ​

FastAPI 中,它被称为路径。 路径或路由是 URL 中第一个"/"之后的部分。

如果参数名出现在路径的 { } 里——>路径参数

使用带有变量参数的路径

lua
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello/{name}")
async def root(name : str):
    return {"message": f("Hello {name}")}

启动 Uvicorn 服务器并访问 http://localhost:8000/hello/Tutorialspoint URL。 浏览器显示以下 JSON 响应。

json
{"name":"Tutorialspoint"}

将变量路径参数更改为其他内容,例如 http://localhost:8000/hello/Python,

json
{"name":"Python"}

多参数路由 ​

python
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello/{name}/{age}")
async def hello(name,age):
   return {"name": name, "age":age}

如果浏览器地址栏中给出的URL是http://localhost:8000/hello/Ravi/20,那么Ravi和20的 数据将分别赋值给变量name和age。 浏览器显示以下 JSON 响应 −

json
{"name":"Ravi","age":"20"}

路径参数校验Path(了解即可) ​

Path用来对路径参数补校验规则和文档描述

gt ——大于 lt ——小于 ge——大于等于 le ——小于等于

例子

python
# 定义请求的的路径,请求方式为get
@app.get("/book/{id}")
# 定义这个接口,接收一个参数id,数据类型为int
async def get_book(id: int=Path(...,gt=0,lt=101)):
    # 返回响应结果
    return {"id": id, "title": f"这是第{id}本书"}

通过docs文档可以知道路径参数必须为int整形且0<id<101


查询参数 ​

声明的参数名没有出现在路径的 { } 里,路径操作函数会把该参数自动解释为查询参数

  • 位置:URL 中的 ? 之后由&拼接的查询字符串,格式为 key1=value1&key2=value2(如 ?page=1&sort=desc)。
  • 作用:对资源集合进行过滤、排序、分页等操作(如查询“第1页、按时间降序”的书籍列表)。
  • 常用方法:GET(用于获取资源集合的筛选结果)。

例

python
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
async def hello(name:str,age:int):
   return {"name": name, "age":age}

截屏2026-09-12 20.38.50.png

docs检查会告诉您 FastAPI 已检测到 /hello 端点没有路径参数,但有查询参数

截屏2026-09-12 20.44.14.png

单击Try it out按钮,输入"xiaowang"和"18"作为值,然后按Execute执行按钮。 文档页面现在显示 Curl 命令、请求 URL 以及 HTTP 响应的正文和标头

截屏2026-09-12 20.47.09.png


查询参数校验Query(了解即可) ​

Query用来对查询参数补校验规则和文档描述

例

python
from fastapi import FastAPI, Query
 
app = FastAPI()
 
@app.get("/books/")
async def read_books(
    # 需求:图书分类,默认值为 "Python开发",长度限制 5 ~ 255
    category: str = Query(
        "Python开发", 
        min_length=5, 
        max_length=255,
        description="图书的分类名称"
    ),
    # 需求:价格,限制大小范围 50 ~ 100
    price: float = Query(
        ..., 
        ge=50, 
        le=100,
        description="图书的价格范围"
    )
):
    return {
        "category": category, 
        "price": price,
        "message": "查询成功"
    }

通过docs文档可以知道查询参数category为str类型,长度限制 5 ~ 255。price为float类型大小范围 50 ~ 100


Pydantic 重点! ​

Pydantic 是一个用于 数据解析和验证的 Python 库。Pydantic 定义了 BaseModel 类。 它充当创建用户定义模型的基类

截屏2026-09-12 21.11.46.png


Pydantic 5 层用法,从最基础到 Agent 实战 ​

  1. 第1层 定义模型

继承 BaseModel 就行。每个字段写 名字: 类型。

python
from pydantic import BaseModel

class ChatRequest(BaseModel):
    message: str
    temperature: float
  1. 第2层 默认值与可选

规则和 Python 函数参数一样:有默认值就是可选,没有就是必填。这条规则同时决定了 /docs 里字段有没有标 "required"。

python
class ChatRequest(BaseModel):
    message: str                      # 没默认值 → 必填
    temperature: float = 0.7          # 有默认值 → 可选
    system_prompt: str | None = None  # 可选,且允许为 None
  1. 第 3 层 · 用 Field 加约束和描述
python
from pydantic import BaseModel, Field

class ChatRequest(BaseModel):
    message: str = Field(min_length=1, max_length=4000, description="用户消息")
    temperature: float = Field(default=0.7, ge=0, le=2)
  • min_length / max_length — 字符串长度
  • ge / le — 大于等于 / 小于等于(gt/lt 是不含等号)
  • description — 会直接显示在 /docs 页面上
  1. 第 4 层 · 嵌套模型与集合(Agent 场景最常用)
python
class Message(BaseModel):
    role: str          # "user" / "assistant" / "system"
    content: str

class ChatRequest(BaseModel):
    messages: list[Message]          # 模型列表
    metadata: dict[str, str] = {}    # 字典

这就是对话历史的形态——一个 Message 列表。FastAPI 会自动把 JSON 数组里的每个元素都校验一遍,包括嵌套的 role 和 content。

  1. 实例与数据间转换
python
from pydantic import BaseModel

class Message(BaseModel):
    role: str
    content: str

m = Message(role="user", content="你好")

# ── 进来(类方法,注意是 Message. 不是 m.)──
Message.model_validate({"role": "user", "content": "你好"})      # → Message 实例
Message.model_validate_json('{"role":"user","content":"你好"}')  # → Message 实例

# ── 出去(实例方法)──
m.model_dump()        # {'role': 'user', 'content': '你好'}   → dict
m.model_dump_json()   # '{"role":"user","content":"你好"}'     → str

model_validate 系列 = 类工厂,负责"把外部不规整的数据校验成规整的 Message 实例";

odel_dump 系列 = 实例出口,负责"把 Message 实例还原成 dict/JSON" 。


一个必学参数:exclude_unset ​

python
class UpdateProfile(BaseModel):
    nickname: str | None = None
    bio: str | None = None

# 用户只想改昵称,只传了 nickname
req = UpdateProfile.model_validate({"nickname": "小王"})

req.model_dump()                    # {'nickname': '小王', 'bio': None}   ← 坑在这
req.model_dump(exclude_unset=True)  # {'nickname': '小王'}               ← 正确

exclude_unset=True 的含义是"只输出用户显式传过的字段",而不是"输出所有字段(没传的给默认值)"。

不用它的后果:你拿这个 dict 去更新数据库,会把用户没碰过的 bio 字段覆盖成 None。这是做 PATCH 类接口的经典事故。


三个坑 ​

坑 1:model_dump_json() 返回的是字符串,不是 dict

python
return JSONResponse(content=req.model_dump_json())   # ❌ 变成一坨双重转义的字符串
return JSONResponse(content=req.model_dump())        # ✅ 要 dict 就用这个

坑 2:model_dump() 是递归的

嵌套模型会被转成嵌套 dict,不会留下模型对象:

python
conv.model_dump()
# {'messages': [{'role': 'user', 'content': '你好'}]}
#              ↑ 里面也是 dict,不是 Message 对象

这通常是你想要的(正好能直接 json 序列化),但如果你期望"外层转 dict、内层还是模型",那就理解错了。

坑 3:model_validate 默认会做类型转换,不是严格校验

python
class Item(BaseModel):
    price: float

Item.model_validate({"price": "9.9"})    # ✅ 自动把 "9.9" 转成 9.9
Item.model_validate({"price": "abc"})    # ❌ 转不了,抛 ValidationError

# 想要严格模式(字符串不接受):
Item.model_validate({"price": "9.9"}, strict=True)   # ❌ 报错

这个"宽松转换"通常是好事(前端传 "5" 也能接住),但如果你做的是金融金额、ID 这类不能容忍隐式转换的场景,记得开 strict=True。

Pydantic 对 AI Agent 工程师真正的杀手级价值——它不只是校验请求,而是你和 LLM 之间的契约层:

截屏2026-09-12 22.39.17.png


代码案例(一定要看懂) ​

python
from pydantic import BaseModel, Field

class SearchArgs(BaseModel):
    """网页搜索工具"""

    # 必填参数:没有默认值,LLM 返回的 JSON 里如果缺 query 字段,校验直接失败
    query: str = Field(description="搜索关键词")

    # 可选参数:default=5 表示不传就用 5;ge=1 / le=20 是限制范围(大于等于 1、小于等于 20)
    # LLM 若返回 limit=100,Pydantic 会立刻抛 ValidationError,防止非法值进到后面代码
    limit: int = Field(default=5, ge=1, le=20, description="返回条数")

tools = [
    {
        "type": "function",          # 固定写法:声明这是一个"函数调用"类型工具
        "function": {
            # 工具名字:LLM 决定调用哪个工具时,就是拿这个名字来匹配的
            "name": "search",
            # 工具说明:用类的文档字符串("""网页搜索工具""")当描述
            # 注意:若类没写文档docstring,__doc__ 会是 None,OpenAI API 会拒绝这个工具
            "description": SearchArgs.__doc__,
            # 参数 schema:根据上面 SearchArgs 的字段定义自动生成 JSON Schema
            # 它会包含 query 必填、limit 可选且范围 1~20、字段 description 等全部信息
            "parameters": SearchArgs.model_json_schema(),
        },
    }
]

def parse_llm_arguments(llm_response: str) -> SearchArgs:
    """
    把 LLM 丢回来的 JSON 字符串,经 Pydantic 校验后转成类型安全的 SearchArgs 实例。
    """
    return SearchArgs.model_validate_json(llm_response)

if __name__ == "__main__":
    # 真实项目中,这一串是 LLM 根据上面 tools 的 JSON Schema 自己生成的
    llm_response: str = '{"query": "FastAPI 模板渲染", "limit": 3}'

    # 回程:字符串 → 校验 → 对象
    args = parse_llm_arguments(llm_response)

    print("=== 回程结果(LLM JSON → 类型安全的 Python 对象)===")
    print("query :", args.query)     # 类型确定是 str,
    print("limit :", args.limit)     # 类型确定是 int

    print("=== 去程产物(模型 → JSON Schema,这就是喂给 LLM 的 parameters)===")
    import json

    print(json.dumps(SearchArgs.model_json_schema(), ensure_ascii=False, indent=2))

校验失败长什么样 ​

不用你处理,FastAPI 自动返回 422:

json
{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "message"],
      "msg": "Field required"
    }
  ]
}

loc 告诉你是哪个字段出的问题(嵌套模型会显示完整路径如 ["body", "messages", 0, "role"]),msg 说明原因。前端可以直接拿这个渲染错误提示。


请求体 ​

  • 位置:HTTP 请求的消息体(body)中,常以 JSON 等格式携带数据。
  • 作用:用于创建、更新资源,或携带大量数据(如提交书籍的详细信息、更新用户资料)。
  • 常用方法:POST(创建资源)、PUT(更新资源)等。

在HTTP协议中,一个完整的请求由三部分组成:

  1. 请求行:包含方法、URL、协议版本
  2. 请求头:元数据信息(Content-Type、Authorization等)
  3. 请求体:实际要发送的数据内容

我们现在将使用 Pydantic 模型对象作为客户端请求的请求体。

python
from fastapi import FastAPI
from typing import List
from pydantic import BaseModel, Field

app = FastAPI()

class Student(BaseModel):
   id: int
   name :str = Field(None, title="name of student", max_length=10)
   subjects: List[str] = []

@app.post("/students/")
async def student_data(s1: Student):
    return s1

可以看出,student_data() 函数由 @app.post() 装饰器装饰,其 URL 端点为"/students/"。它从客户端的请求中接收一个 Student 类的对象作为 Body 参数。 要测试此路由,请启动 Uvicorn 服务器并通过访问 http://localhost:8000/docs 在浏览器中打开 Swagger UI 文档

截屏2026-09-12 23.11.12.png

文档指出"/students/"路由附加了带有 POST 方法的 student_data() 函数。 在 schemas 部分下,将列出 Student 模型。

展开它前面的节点以显示模型的结构

截屏2026-09-12 23.12.06.png

点击Try it out按钮,在请求主体中填写测试值。

截屏2026-09-12 23.15.03.png

单击Execute执行按钮并获取服务器的响应值。

截屏2026-09-12 23.15.33.png


练习 ​

需求:设计接口新增图书,图书信息包含:书名、作者、出版社、售价

具体要求如下:

 书名:不能为空;长度 2 ~ 20

 作者:长度 2 ~ 10

 出版社:默认值“小王出版社”

 售价:不能为空;价格大于0元

python
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
 
app = FastAPI()
 
# 1. 定义数据模型
class Book(BaseModel):
    # 书名:不能为空;长度 2 ~ 20
    book_name: str = Field(
        ..., 
        min_length=2, 
        max_length=20, 
        description="书名,必填,2-20字"
    )
    # 作者:长度 2 ~ 10
    author: str = Field(
        ..., 
        min_length=2, 
        max_length=10, 
        description="作者,必填,2-10字"
    )
    # 出版社:默认值“黑马出版社”
    publisher: str = Field(
        default="小王出版社", 
        description="出版社,默认小王出版社"
    )
    # 售价:不能为空;价格大于0元
    price: float = Field(
        ..., 
        gt=0, 
        description="价格,必填,大于0"
    )
 
# 2. 定义新增图书接口
@app.post("/books", summary="新增图书")
async def create_book(book: Book):
    """
    接收图书信息,校验通过后返回
    """
    # 这里可以添加保存到数据库的逻辑
    return {"message": "图书新增成功", "book_data": book}

异常处理 ​

FastAPI 异常处理的整体链路(从请求到响应) ​

一次请求大致会经历:

  1. 路由匹配(是否有对应 path + method)
  2. 依赖注入与参数解析
    • 路径/查询/请求头/Cookie/Body 参数解析
    • Pydantic 校验与类型转换
  3. 执行你的路径函数(endpoint)
  4. 返回值序列化为响应(response_model、JSON 序列化等)
  5. 中间件(Middleware)前后包裹(异常也会在这里被捕获/转换)

FastAPI 默认会处理的几类“常见异常” ​

A. 参数校验失败:422 Unprocessable Entity(最常见) ​

来源:Pydantic / 参数解析阶段

典型场景:

  • Body 缺字段、类型不匹配
  • Query/Path/Header 校验(例如 Query(ge=0) 不满足)
  • 嵌套模型校验失败

表现:FastAPI 自动返回 422,响应结构通常是:

json
{
  "detail": [
    {
      "loc": ["body", "fieldName"],
      "msg": "Field required",
      "type": "missing"
    }
  ]
}

要点:

  • loc 指出错误位置(body/query/path/header + 字段路径)
  • msg 是人类可读信息
  • 这类异常本质是 RequestValidationError

B. 主动抛出 HTTPException:自定义状态码与错误信息 ​

这是 FastAPI 推荐的“业务层错误抛法”。

python
from fastapi import HTTPException

raise HTTPException(status_code=404, detail="资源不存在")

特点:

  • 会被框架直接转换成对应 status code 的 JSON 响应
  • detail 是返回体核心字段(你也可以放 dict)

典型用法:

  • 鉴权失败:401/403
  • 资源不存在:404
  • 业务规则不满足:400/409 等

截屏2026-09-13 23.58.47.png


C. 路由不存在:404 Not Found ​

当 path/method 不匹配时,Starlette 层返回 404。


D. 未捕获的运行时异常:500 Internal Server Error ​

比如你写的 endpoint 里 1/0、访问空对象等,没有 try/except,也没有全局 handler。

生产环境通常不返回 traceback 给客户端(避免泄露内部信息)。


FastAPI —UploadFile文件上传 ​

  • 读取方式——流式读取
  • 获取的类型——文件对象
  • 适合大小——任意大小

UploadFile:文件存在磁盘临时位置,不会把内存撑爆

python
from fastapi import FastAPI, UploadFile

app = FastAPI()

@app.post("/upload")
async def upload(file: UploadFile):
    # 1. 读内容
    content = await file.read()

    # 2. 存到本地磁盘
    with open(f"{file.filename}", "wb") as f:
        f.write(content)

    # 3. 返回信息给客户端
    return {"filename": file.filename, "size": file.size or len(content), "type": file.content_type}

用await file.read() 读取文件,再一次性写入磁盘。这对小文件没问题,但大文件会占满内存 。

shutil.copyfileobj 会 分块流式拷贝 (默认 64KB buffer),边读边写,内存占用恒定。

python
from fastapi import FastAPI, UploadFile
import shutil

app = FastAPI()

@app.post("/upload")
async def upload(file: UploadFile):
    # 流式拷贝:边读边写,不占满内存
    with open(file.filename, "wb") as f:
        shutil.copyfileobj(file.file, f)

    return {"filename": file.filename, "size": file.size, "type": file.content_type}

多个文件 ​

python
@app.post("/upload-many")
async def upload_many(files: list[UploadFile]):
    return {"count": len(files)}

⚠️ 两个安全点,别偷懒 ​

第一,校验类型:

python

python
ALLOWED = {"image/png", "image/jpeg", "application/pdf"}

@app.post("/upload")
async def upload(file: UploadFile):
    if file.content_type not in ALLOWED:
        raise HTTPException(400, f"不支持的类型:{file.content_type}")

第二,绝对不要直接用 file.filename 拼路径:

python

python
# ❌ 危险:用户传 "../../../etc/passwd" 就能写到系统目录
open(f"./uploads/{file.filename}", "wb")

# ✅ 自己生成文件名,只保留扩展名
import uuid, os
ext = os.path.splitext(file.filename)[1]
safe_name = f"{uuid.uuid4().hex}{ext}"
open(f"./uploads/{safe_name}", "wb")

"""
- `os.path.splitext(file.filename)[1]` —— 只拿扩展名。比如`../../../etc/passwd` 切出来 ext 是`""` (没有扩展名),`evil.png` 切出来是`.png` 。`../` 这些危险字符都在"主文件名"部分,被直接丢弃了。
- `uuid.uuid4().hex` —— 生成一个 32 位的随机十六进制字符串,比如`a3f5c2e1b9...` 。这串字符 只包含 0-9 和 a-f ,不可能出现`/` 、`..` 、空格等路径特殊字符。
- 拼起来 :`a3f5c2e1b9....png` —— 一个纯安全字符组成的文件名,无论用户传什么恶意路径,最终落到磁盘上的都是这种安全名字。
"""

这叫路径穿越攻击,是文件上传功能的头号漏洞。


FastAPI - Header 标头参数 ​

为了读取作为客户端请求的一部分的 HTTP header 标头的值,从 FastAPI 库中导入 Header 对象,并在操作函数定义中声明一个 Header 类型的参数。

python
from typing import Annotated

from fastapi import FastAPI, Header

app = FastAPI()

# 写法一:直接 `Header()` 作为默认值(经典写法)
# 注意:Header 不带默认值 = 必填,请求缺少 X-Token 头会直接返回 422
@app.get("/items")
async def read_items(x_token: str = Header()):
    return {"token": x_token}

# 写法二:用 `Annotated` 声明(必填)
# `Annotated[str, Header()]` 把"类型"和"参数来源"打包在一起,类型信息更完整
# 不传 Authorization 头 → 422
@app.get("/required-header")
async def required_header(authorization: Annotated[str, Header()]):
    return {"authorization": authorization}

# 写法三:`Annotated` + 可选
# 注意:`Annotated` 写法下默认值用 `= None` 设置,而不是 Header(default=None)
# 该头可以不传,此时参数值为 None,不会报错
@app.get("/optional-header")
async def optional_header(user_agent: Annotated[str | None, Header()] = None):
    return {"user_agent": user_agent}

# 写法四:`Annotated` + 默认值
# 同理,默认值 "zh-CN" 写在 `= "zh-CN"`,不传 Accept-Language 头时自动使用
@app.get("/default-header")
async def default_header(accept_language: Annotated[str, Header()] = "zh-CN"):
    return {"accept_language": accept_language}

核心规则:下划线 ——> 连字符 ​

截屏2026-09-13 23.16.19.png

HTTP 请求头用连字符(X-Token),Python 变量名不能含连字符(只能 x_token)。 FastAPI 自动帮你转换:

转换方向是单向的:参数名里的 _ 会被换成 -。所以:

  • ✅ x_token: str = Header() → 匹配 X-Token
  • ❌ 如果你写 x-token: str = Header() → Python 语法错误,根本写不出来

预定义标头。 ​

操作函数应该有一个Response类型的参数。在以下情况下,将添加一个名为"X-Web-Framework"的自定义标头和一个预定义标头"Content-Language"以及操作功能的响应。

python
from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/rspheader/")
def set_rsp_headers():
   content = {"message": "Hello World"}
   headers = {"X-Web-Framework": "FastAPI", "Content-Language": "en-US"}
   return JSONResponse(content=content, headers=headers)

校验与描述 ​

Header() 接受和 Field() 完全一样的参数:

python
from typing import Annotated

from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/key")
async def read_key(x_api_key: Annotated[str, Header(
    min_length=10,
    max_length=64,
    description="你的 API Key,在控制台获取",
    deprecated=False,
)]):
    return {"api_key": x_api_key}

description 会直接显示在 /docs 页面,这对使用方非常友好


实战:Bearer Token 鉴权 ​

这是 Header() 在 Agent 项目里 90% 的用途:

python
from fastapi import Header, HTTPException

API_KEYS = {"sk-abc123": "user-001", "sk-def456": "user-002"}

@app.post("/chat")
async def chat(
    req: ChatRequest,
    authorization: str | None = Header(
        default=None,
        description="Bearer <你的 API Key>",
    ),
):
    if authorization is None:
        raise HTTPException(401, "缺少 Authorization 请求头")

    if not authorization.startswith("Bearer "):
        raise HTTPException(401, "格式错误,应为:Bearer <token>")

    token = authorization.removeprefix("Bearer ").strip()
    user_id = API_KEYS.get(token)

    if user_id is None:
        raise HTTPException(
            status_code=401,
            detail="无效的 API Key",
            headers={"WWW-Authenticate": "Bearer"},   # 401 的标准响应头
        )

    # 鉴权通过,user_id 可以用于限流、计费、日志
    return await run_agent(req, user_id=user_id)

几个细节值得注意:

  • removeprefix("Bearer ") 比 replace("Bearer ", "") 更安全——后者会把 token 中间的 "Bearer " 也替换掉
  • .strip() 去掉可能的多余空格
  • 401 响应带 WWW-Authenticate: Bearer 是 HTTP 标准要求,客户端库会据此识别认证方式

常见真实请求头清单 ​

请求头用途Agent 相关性
Authorization鉴权 token(Bearer / Basic)高
X-API-Key简单 API Key高
X-Request-ID请求追踪,配合中间件打日志高
Idempotency-Key幂等键,防重复调用中
User-Agent客户端标识、限流依据中
Accept-Language语言偏好低
X-Forwarded-For真实客户端 IP(在 Nginx 后面时)中

四个坑 ​

坑 1:命名写错不报错,只是取不到值

坑 2:自定义请求头会触发 CORS 预检

这个在 Agent 项目里特别容易中招。跨域调用时,浏览器发现请求带了非简单头(Authorization、X-Request-ID 都属于此类),会先发一个 OPTIONS 预检请求。如果你的 CORS 配置没放行,浏览器会直接拦掉——后端连日志都不会有,你会以为是接口写错了。

python

python
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://your-frontend.com"],
    allow_headers=["Authorization", "X-Request-ID"],   # ← 自定义头必须显式列出
    # 开发阶段图省事可以写 ["*"]
)

坑 3:请求头只能装字符串,塞不了 JSON

python

python
# ❌ 前端没法把对象放进请求头
X-Config: {"a": 1}

# ✅ 只能塞扁平字符串,需要结构化数据就编码
X-Config: eyJhIjoxfQ==      # base64 编码后的 JSON

真需要传结构化数据,请走请求体,不要硬塞请求头。

坑 4:别把敏感内容写进日志

Authorization 头里装的就是 token。如果你的中间件无脑打印所有请求头,等于把用户密钥写进了日志文件。打日志时记得脱敏:

python

python
safe_headers = {
    k: ("***" if k.lower() == "authorization" else v)
    for k, v in request.headers.items()
}

一个进阶替代方案 ​

实际项目里,鉴权更推荐用 Security + HTTPBearer,因为它会自动在 /docs 页面加上一个"Authorize"按钮,测试接口时不用手动敲请求头:

python

python
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import Security

bearer = HTTPBearer()

@app.post("/chat")
async def chat(
    req: ChatRequest,
    creds: HTTPAuthorizationCredentials = Security(bearer),
):
    token = creds.credentials   # 已经帮你剥掉 "Bearer " 了

差别:Header() 给你原始字符串(完全可控),Security(HTTPBearer()) 给你解析好的凭据 + 文档集成(更省事)。建议先掌握 Header() 的原理,实际项目用 HTTPBearer。


总结一句话:Header() 的核心就三件事——下划线转连字符、请求头名不区分大小写、值永远是字符串。把这三条记住,剩下的都是 Field() 那套校验参数的复用。