写 FastAPI 接口的时候,大部分场景我们都靠 Pydantic 模型自动帮忙做参数校验,几乎不用手动碰原始请求。但总有些时候,比如要拿到客户端 IP、读取原始请求头、判断连接是否断开,这时候就得请出 Request 这个类。它是 FastAPI 留给开发者的一扇后门,绕过所有校验逻辑,直接摸到 ASGI 层面的原始数据。下面就把 Request 类里里外外拆开看看。
Request 从哪来,长什么样
Request 其实不是 FastAPI 自己发明的东西,它是从底层的 ASGI 框架 Starlette 那里继承过来的,FastAPI 只是把它重新导出了一遍,方便你直接 from fastapi import Request 来用。
理解 Request 之前得先明白 ASGI 是个啥。简单说,ASGI 就是 Web 服务器和 Python 框架之间约定的一套通信协议,每次有请求进来,服务器会传给框架三样东西:一个叫 scope 的字典(装着这次连接的所有元信息,比如方法、路径、请求头)、一个叫 receive 的异步函数(用来一点点接收请求体数据)、还有一个叫 send 的异步函数(用来往外发响应)。Request 类做的事情,就是把这三样原始材料包装成一个好用的对象,让你不用直接跟这些底层细节打交道。
从继承关系上看,Request 是 HTTPConnection 的子类,专门针对 HTTP 协议做了扩展(WebSocket 连接则对应另一个类)。用一张类图能看得更清楚
可以看到,比较通用的属性(比如 url、headers、client 这些)都定义在父类 HTTPConnection 里,因为 WebSocket 连接也需要这些信息;而只有 HTTP 请求才独有的东西,比如读取请求体的方法、判断方法类型的 method,才放在 Request 子类里。
核心属性一览,请求的骨架信息
Request 对象身上挂了一大堆只读属性(Python 里叫 property),大部分不需要调用,直接访问就行。整理成表格会更直观
| 属性 | 类型/说明 | 用途举例 |
|---|---|---|
scope |
原始 ASGI 字典 | 最底层数据,一般不会直接用 |
app |
FastAPI 应用实例本身 | 通过 request.app.state 拿全局共享数据 |
url |
URL 对象 | 拿到完整请求地址,比如 request.url.path |
base_url |
基础 URL | 拼接绝对链接时用 |
headers |
请求头字典 | 读取自定义 Header,比如鉴权 Token |
query_params |
查询参数 | 类似 ?page=1&size=10 里的内容 |
path_params |
路径参数 | 路由里 {item_id} 匹配出来的值 |
cookies |
Cookie 字典 | 读取浏览器带来的 Cookie |
client |
客户端地址 | 拿到访问者的 IP 和端口 |
session |
会话数据 | 需配合 SessionMiddleware 使用 |
auth / user |
鉴权信息 | 需配合 AuthenticationMiddleware 使用 |
state |
自定义状态容器 | 在中间件和路由函数之间传递临时数据 |
method |
HTTP 方法 | GET、POST 之类的字符串 |
这里面 session、auth、user 三个属性有点特殊,它们默认是空的,只有你在应用里挂载了对应的中间件(SessionMiddleware 或 AuthenticationMiddleware)之后才会被填充数据,不然访问会报错。
state 属性则很实用,它是一个可以随便塞东西的容器对象,常见套路是在中间件里往 request.state 塞一个数据库连接或者请求 ID,然后在后面的路由函数里直接取出来用,省去了层层传参的麻烦。
读取请求体的四种姿势,stream、body、json、form
请求体(body)这部分是 Request 类设计得最讲究的地方,因为网络数据是一段一段流式传过来的,不可能一下子全拿到。Request 提供了四个层层递进的方法来处理这件事。
stream() 是最底层的方式,它是一个异步生成器,每次 async for chunk in request.stream() 都会拿到一小块字节数据,适合处理超大文件上传这种不想一次性塞进内存的场景。
body() 则是把 stream 产生的所有片段拼起来,缓存成一个完整的 bytes 对象再返回给你,第一次调用之后结果会被缓存,后面再调用不会重复读取网络流。
json() 在 body() 的基础上再做一层,自动把拿到的字节串用 json 库解析成 Python 字典或列表,等于是 json.loads(await request.body()) 的语法糖。
form() 用来解析表单数据,不管是普通的 urlencoded 表单还是带文件的 multipart 表单都能处理,返回一个 FormData 对象。
整个读取过程用时序图表示会更清楚
有个细节容易被忽略,body 只能被完整消费一次,如果你先手动用 stream() 读了一遍,后面再调用 body() 或 json() 就会报错,因为流已经空了。所以真要自定义读取逻辑,得想清楚只用一种方式贯穿到底。
生命周期与特殊方法,断线检测和路由反查
除了读数据,Request 还提供几个跟连接状态、路由系统打交道的方法,日常用得不算特别频繁,但关键场景离不开它们。
is_disconnected() 是个异步方法,专门用来检测客户端是不是已经断开连接。它在处理服务器推送事件(SSE)或者长时间流式响应的时候特别有用,可以在循环里不断检查,一旦客户端断了就及时停止发送、节省服务器资源。
close() 用来关闭请求过程中可能打开的资源,比如 multipart 表单解析时创建的临时文件句柄,FastAPI 内部一般会自动帮你调用,通常不需要手动管理。
send_push_promise() 是针对 HTTP/2 协议的服务器推送功能,允许在客户端还没主动请求某个资源之前,服务器就主动把资源推过去,不过这个功能依赖底层 ASGI 服务器本身支持 HTTP/2,用的场景相对少见。
url_for() 则是路由反查工具,给定路由名字和参数,直接算出对应的 URL,比手写字符串拼接靠谱多了,尤其是路由路径以后要改动的时候,用 url_for 写的代码完全不受影响。
receive 属性则是最原始的 ASGI 接收通道,一般只有自己实现底层协议解析的时候才会直接用到它,普通业务代码基本碰不到。
一个简单例子
用一段代码把上面讲的东西串起来,感受一下实际使用的样子
python
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_client_ip(request: Request, call_next):
# 往state里塞一个自定义数据,供后面路由使用
request.state.client_ip = request.client.host
response = await call_next(request)
return response
@app.post("/echo")
async def echo(request: Request):
body_bytes = await request.body()
headers = dict(request.headers)
return {
"method": request.method,
"path": request.url.path,
"client_ip": request.state.client_ip,
"body_size": len(body_bytes),
"headers": headers,
}
这段代码里,中间件读取了客户端 IP 存进 state,路由函数再把它取出来一起返回,顺便展示了 method、url、body、headers 这几个最常用属性的取法。
什么时候该用 Request,什么时候不用
说到底,Pydantic 模型帮你把大部分脏活累活都干了,能用模型自动校验就尽量用模型,代码更简洁,文档生成也更漂亮。但遇到下面这几种情况,Request 就是唯一的解法
- 需要拿到原始请求头或者一些框架没内置支持的元信息
- 要做流式读取,处理超大文件或者不确定格式的数据
- 需要判断客户端是否断开连接,做长连接、SSE 之类的场景
- 需要在中间件和路由之间传递自定义状态
理解了这些边界,用起来就会得心应手很多,Request 也就不再是个神秘的黑盒子了。
FastAPI Reference Request class fastapi.tiangolo.com/reference/r...
Starlette Requests documentation starlette.dev/requests/
Starlette docs gist(含 body/json/stream/is_disconnected 详细说明)gist.github.com/jph00/07913...
Stack Overflow,关于以编程方式实例化 Starlette Request 并读取 body 的讨论 stackoverflow.com/questions/6...