news 2026/9/23 9:55:25

Starlette 响应体系全解:从 Response 基类到流式与文件响应的源码级实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Starlette 响应体系全解:从 Response 基类到流式与文件响应的源码级实践

Starlette 响应体系全解:从 Response 基类到流式与文件响应的源码级实践

【免费下载链接】starletteThe little ASGI framework that shines. 🌟项目地址: https://gitcode.com/gh_mirrors/st/starlette

Starlette 提供了一套完整的响应类家族,统一通过向 ASGI 的send通道发送http.response.starthttp.response.body消息来返回 HTTP 响应。本文以 docs/responses.md 为主线,结合 starlette/responses.py 源码与 tests/test_responses.py 测试用例,逐一剖析ResponseHTMLResponsePlainTextResponseJSONResponseRedirectResponseStreamingResponseFileResponse的设计与实战要点,读完即可掌握:如何构造带自动响应头的响应、如何操作 Cookie、如何定制 JSON 序列化、如何流式传输内容、如何利用文件响应的 Range 断点续传能力,以及如何挂载后台任务。

响应类总览:一切皆可被调用的 ASGI 应用

在 Starlette 中,所有响应对象都实现了__call__(scope, receive, send)方法,因此响应实例本身就是一个合法的 ASGI 应用。你可以在async def app(scope, receive, send)中直接await response(scope, receive, send)把它发送出去,也可以把响应对象直接交给路由、TestClient 或任何 ASGI 服务器执行。

Response基类位于 starlette/responses.py,其发送流程在__call__中一目了然(源码 L163-L170):

async def __call__(self, scope, receive, send): if scope["type"] == "websocket": send = self._wrap_websocket_denial_send(send) await send({"type": "http.response.start", "status": self.status_code, "headers": self.raw_headers}) await send({"type": "http.response.body", "body": self.body}) if self.background is not None: await self.background()

这段代码还透露了两个重要细节:

  • WebSocket 场景的拒绝响应:当 scope 类型为websocket时,响应消息会被_wrap_websocket_denial_send改写为websocket.http.response.start/websocket.http.response.body,从而向客户端发送 HTTP 拒绝响应(如 403/405),这是 WebSocket 握手失败时的标准反馈机制;
  • 后台任务:响应体发送完毕后,若构造时传入了background(来自 starlette/background.py 的BackgroundTask),会继续执行该任务——这是"响应后清理/记账/通知"等场景的官方挂载点。

Response 基类:签名与自动响应头

Response的构造签名与文档一致:

Response(content, status_code=200, headers=None, media_type=None)
  • content:字符串或字节串(源码render同时支持bytes | memoryview,L48-L53);传入None时渲染为空字节串;
  • status_code:整数 HTTP 状态码,默认200
  • headers:字符串字典;
  • media_type:媒体类型字符串,例如"text/html"

此外源码中还有一个文档之外的隐藏参数background: BackgroundTask | None = None(L39),用于挂载后台任务,前文已述。

Content-Length 与 Content-Type 的自动填充

init_headers(L55-L81)负责生成响应头,规则如下:

  • Content-Length 自动计算:基于渲染后的self.body长度。但有三个例外不会填充Content-Length:状态码小于 200(如 1xx)、状态码为204(No Content)、状态码为304(Not Modified);
  • Content-Type 自动推导:基于media_type;若媒体类型以text/开头且尚未显式包含charset=,则自动追加; charset=utf-8Response.charset类属性默认"utf-8");
  • 去重保护:如果你在headers中已显式提供content-lengthcontent-type,Starlette 不会重复生成,以你传入的值为准;
  • 大小写规范化:传入的 headers 键会被转成小写并以latin-1编码保存为raw_headers(ASGI 要求的list[tuple[bytes, bytes]]格式)。

以下是最小完整示例(文档原样保留,可直接作为独立 ASGI 应用运行):

from starlette.responses import Response async def app(scope, receive, send): assert scope['type'] == 'http' response = Response('Hello, world!', media_type='text/plain') await response(scope, receive, send)

对应测试见 test_text_response 与 test_bytes_response:字符串内容返回response.text,字节内容(如media_type="image/png")返回response.content

动态修改响应头

响应对象还提供headers属性(MutableHeaders),发送前可随时增删改:

response = Response("hello, world", media_type="text/plain", headers={"x-header-1": "123"}) response.headers["x-header-2"] = "789"

测试 test_response_headers 验证了这种先构造后修改的用法,两个自定义头都会出现在最终响应中。

Cookie 操作:set_cookie 与 delete_cookie

Starlette 在响应对象上提供了set_cookie方法(L89-L132),底层基于标准库http.cookies.SimpleCookie生成符合规范的Set-Cookie头,并直接追加到raw_headers

完整签名如下:

Response.set_cookie(key, value, max_age=None, expires=None, path="/", domain=None, secure=False, httponly=False, samesite="lax", partitioned=False)
参数类型说明
keystrCookie 的键
valuestrCookie 的值
max_ageint(可选)Cookie 存活秒数;负数或0会立即丢弃该 Cookie
expiresint / datetime(可选)整数表示距过期的秒数,或直接传一个datetime对象(源码中会通过format_datetime(expires, usegmt=True)格式化为 GMT 字符串,L107-L110)
pathstr(可选)Cookie 生效的路径子集,默认"/"
domainstr(可选)Cookie 有效的域名
securebool(可选)仅在 SSL/HTTPS 请求下发送
httponlybool(可选)禁止通过 JavaScript 的Document.cookieXMLHttpRequestRequestAPI 访问
samesitestr(可选)取值'lax''strict''none',默认'lax'
partitionedbool(可选)跨站 Cookie 仅在最初设置的顶层上下文中可用(CHIPS 规范);仅 Python 3.14+ 支持,否则抛出ValueError

关于samesite,源码 L120-L124 中有硬性断言,非法值会在发送前直接AssertionError;关于partitioned,源码 L126-L129 在sys.version_info < (3, 14)时抛出ValueError("Partitioned cookies are only supported in Python 3.14 and above."),测试 test_set_cookie_raises_for_invalid_python_version 也针对 <3.14 环境做了跳过与校验。

delete_cookie:快速作废 Cookie

delete_cookie是对set_cookie的便捷封装(L134-L152),它将max_age=0expires=0重新调用set_cookie,从而让浏览器立即过期并删除该 Cookie:

Response.delete_cookie(key, path='/', domain=None)

签名中同样可以透传securehttponlysamesite参数(源码实现默认沿用lax)。对应测试见 test_delete_cookie。

HTMLResponse 与 PlainTextResponse:最常用的两个快捷类

这两个类都是Response的极简子类,唯一的区别是预设了media_type类属性(L173-L178):

  • HTMLResponsemedia_type = "text/html",接收文本或字节,返回 HTML 页面;
  • PlainTextResponsemedia_type = "text/plain",返回纯文本。
from starlette.responses import HTMLResponse async def app(scope, receive, send): assert scope['type'] == 'http' response = HTMLResponse('<html><body><h1>Hello, world!</h1></body></html>') await response(scope, receive, send)
from starlette.responses import PlainTextResponse async def app(scope, receive, send): assert scope['type'] == 'http' response = PlainTextResponse('Hello, world!') await response(scope, receive, send)

由于media_typetext/开头,基类的init_headers会自动补上charset=utf-8。这也解释了为什么PlainTextResponse常被框架内部用于返回错误文本——例如 starlette/routing.py 的 404 响应、L279 的 405 Method Not Allowed 响应,以及 FileResponse 内部的 400/416 错误响应。

JSONResponse:默认序列化与自定义 render

JSONResponse接收任意可被 JSON 序列化的数据,返回application/json编码的响应(L181-L201)。

from starlette.responses import JSONResponse async def app(scope, receive, send): assert scope['type'] == 'http' response = JSONResponse({'hello': 'world'}) await response(scope, receive, send)

render方法揭示了默认序列化策略(L194-L201):

def render(self, content: Any) -> bytes: return json.dumps( content, ensure_ascii=False, allow_nan=False, indent=None, separators=(",", ":"), ).encode("utf-8")

要点:

  • ensure_ascii=False:非 ASCII 字符(如中文)不会被转义为\uXXXX,直接以 UTF-8 输出,体积更小、可读性更好;
  • allow_nan=False:序列化时遇到NaN/Infinity会直接抛出ValueError,防止生成非法 JSON;
  • separators=(",", ":"):紧凑输出,去掉多余空格;
  • 测试 test_json_none_response 验证了JSONResponse(None)会正确返回字面量b"null",即"空响应"也是合法 JSON。

自定义 JSON 序列化:子类化并覆写 render

当默认的json.dumps不够用时——例如要序列化datetimeUUID等非标准对象,或想换用更快的序列化库——最佳实践是子类化JSONResponse并覆写render方法。文档给出的 orjson 示例(此处 orjson 为第三方包,需自行安装):

from typing import Any import orjson from starlette.responses import JSONResponse class OrjsonResponse(JSONResponse): def render(self, content: Any) -> bytes: return orjson.dumps(content)

render的职责是把任意content变成bytes,只要返回类型正确,基类的Content-Length计算、响应头生成等逻辑都会照常工作。不过需要说明的是:在绝大多数场景下应优先使用默认JSONResponse,只有在微优化某个特定接口、或确实需要序列化非标准对象类型时,才值得引入自定义 render。

RedirectResponse:默认 307 与 URL 编码

RedirectResponse返回 HTTP 重定向,默认状态码为307 Temporary Redirect(L204-L213)。307 与 302 的关键区别在于:307 会保留原始请求的 HTTP 方法与请求体,适合 POST 等非 GET 方法的重定向场景。

from starlette.responses import PlainTextResponse, RedirectResponse async def app(scope, receive, send): assert scope['type'] == 'http' if scope['path'] != '/': response = RedirectResponse(url='/') else: response = PlainTextResponse('Hello, world!') await response(scope, receive, send)

实现要点:

  • 构造时以空内容初始化(content=b""),因此响应体的Content-Length0,测试 test_redirect_response_content_length_header 专门验证了这一点;
  • Location头由quote(str(url), safe=":/%#?=@[]!$&'()*+,;")生成(L213),保留 URL 中的合法保留字符。测试 test_quoting_redirect_response 验证了/I ♥ Starlette/会被正确编码为/I%20%E2%99%A5%20Starlette/
  • url参数接受字符串或URL对象(来自 starlette/datastructures.py)。注意:RedirectResponse没有独立的media_type参数,若需要自定义媒体类型需通过headers传入。

另外,RedirectResponse在框架内部也有应用:starlette/routing.py 会在末尾斜杠重定向(redirect_slashes=True)时构造RedirectResponse(url=str(redirect_url)),把客户端导流到带斜杠的规范化路径。

StreamingResponse:流式传输异步或同步迭代器

StreamingResponse接受异步生成器/异步迭代器普通生成器/迭代器,将响应体分块流式发送(L222-L283),适用于 SSE、大文件导出、长轮询、逐块生成的 HTML 等"边生成边发送"的场景。

文档示例(每个数字之间延迟 0.5 秒逐步推送):

from starlette.responses import StreamingResponse import asyncio async def slow_numbers(minimum, maximum): yield '<html><body><ul>' for number in range(minimum, maximum + 1): yield '<li>%d</li>' % number await asyncio.sleep(0.5) yield '</ul></body></html>' async def app(scope, receive, send): assert scope['type'] == 'http' generator = slow_numbers(1, 10) response = StreamingResponse(generator, media_type='text/html') await response(scope, receive, send)

同步迭代器与 file-like 对象

源码构造逻辑(L233-L236)会判断传入内容是否为AsyncIterable

  • 是 → 直接作为body_iterator
  • 否(同步迭代器)→ 用iterate_in_threadpool包装(来自 starlette/concurrency.py),把同步迭代放到线程池中执行,避免阻塞事件循环。

因此,file-like 对象(例如open()返回的文件对象)本质上是普通迭代器,可以直接传给StreamingResponse进行流式读取。测试 test_sync_streaming_response 验证了同步生成器同样可用;test_streaming_response_custom_iterator 与 test_streaming_response_custom_iterable 则验证了任意实现了__aiter__/__anext__的自定义异步迭代器/可迭代对象均可作为内容源。

流式发送协议细节

stream_response方法(L248-L255)是流式发送的核心:

  • 先发送http.response.start
  • 逐块迭代:非bytes的块会自动用charset(默认 utf-8)编码,并以more_body=True标记"还有后续";
  • 最后发送空的http.response.bodymore_body=False收尾。

由于响应体不是一次性渲染的,基类的自动Content-Length不会生效(没有完整的 body 可计算长度),流式响应默认使用 chunked 传输。

客户端断连与 ASGI 版本适配

源码针对 ASGI 协议版本做了分叉处理(L265-L283):

  • asgi.spec_version >= (2, 4):流式发送中若抛出OSError(典型如对端已断开导致的写失败),会转换为ClientDisconnect异常(定义于 starlette/requests.py),供上层精确捕获处理;
  • 更早的协议版本:通过create_collapsing_task_group(来自 starlette/_utils.py)同时监听http.disconnect消息并推进响应流,一旦任一方完成即取消另一任务组。

这保证了大响应流式推送时,客户端中途断开不会让服务器悬挂。StreamingResponse同样支持background后台任务,测试 test_streaming_response 验证了响应体("1, 2, 3, 4, 5")与后台任务("6, 7, 8, 9")各自独立完成。

FileResponse:异步文件流式响应与 HTTP Range 断点续传

FileResponse异步流式传输文件,其构造参数与其他响应类型不同(L296-L329):

参数说明
path要流式传输的文件路径
headers自定义响应头字典
media_type媒体类型字符串;未指定时根据filenamepath通过mimetypes.guess_type推断(L314-L316),推断失败时回退为application/octet-stream
filename若设置,会出现在响应的Content-Disposition中,实现"下载文件名"效果
content_disposition_typeContent-Disposition的类型,"attachment"(默认,触发下载)或"inline"(浏览器内联展示)
background后台任务(源码扩展参数)
stat_result可选的预计算文件统计信息(源码扩展参数),传入后跳过运行时os.stat
from starlette.responses import FileResponse async def app(scope, receive, send): assert scope['type'] == 'http' response = FileResponse('statics/favicon.ico') await response(scope, receive, send)

自动生成的响应头

文件响应会自动包含四类头(set_stat_headers,L331-L339):

  • Content-Length:来自stat_result.st_size
  • Last-Modified:来自文件 mtime,格式化为 HTTP-date;
  • ETag:由md5(str(st_mtime) + "-" + str(st_size))生成,即"修改时间 + 文件大小"的哈希,文件未变则 ETag 不变;
  • Accept-Ranges: bytes:在__init__中通过setdefault添加(L319),声明支持字节范围请求。

Content-Type则由传入的media_type或按文件名推断;若filename非空,还会生成Content-Disposition头(L320-L326):ASCII 安全时输出attachment; filename="example.png",含非 ASCII 字符(如中文文件名你好.txt)时输出 RFC 5987 风格的attachment; filename*=utf-8''...,对应测试见 test_file_response 与 test_file_response_with_chinese_filename。

运行时文件校验

如果构造时未提供stat_result__call__会在线程池中执行os.stat并填充上述头(L348-L359),同时做两项校验:

  • 文件不存在 →RuntimeError(f"File at path {self.path} does not exist.")
  • 路径不是普通文件(如目录)→RuntimeError(f"File at path {self.path} is not a file.")

测试 test_file_response_with_missing_file_raises_error 与 test_file_response_with_directory_raises_error 分别覆盖了这两种异常。

HTTP Range 请求:206 / 416 与多段响应

FileResponse完整支持 HTTP 范围请求,这是文档特别强调的能力:

  • 请求带Range头且文件存在时,返回206 Partial Content,只传输请求的字节区间;
  • 范围不合法(如起始位置超出文件大小)时,返回416 Range Not Satisfiable,并附带Content-Range: bytes */{file_size}头(L372-L374);
  • 同时支持If-Range条件判断(L363-L365):只有当If-Range值等于当前的Last-ModifiedETag时才处理 Range,否则返回完整文件(_should_use_range,L456-L457)。

Range 处理逻辑(__call__的 L361-L382)分支如下:

  1. Range头(或有If-Range但条件不满足)→ 完整文件响应(_handle_simple);
  2. Range头解析失败 → 400(MalformedRangeHeader);
  3. 单个有效范围 →206单段响应(_handle_single_range),重写Content-Range: bytes {start}-{end-1}/{file_size}与新的Content-Length(L401-L418);
  4. 多个范围 →206Content-Type变为multipart/byteranges; boundary=...的多段响应(_handle_multiple_ranges,L420-L454),boundary 由token_hex(13)生成,每段携带独立的Content-TypeContent-Range

范围解析规则(_parse_range_header/_parse_ranges,L459-L528)还有几个工程细节:

  • 只支持bytes单位,其他单位返回 400;
  • 请求段数超过max_ranges = 100时直接忽略 Range、返回完整文件;
  • 支持-500(末尾 500 字节)这类无起始位置的后缀范围语法;
  • 多个重叠范围会被合并,避免重复传输;
  • 空段(如-)与非数字段会被忽略。

此外FileResponse对两类特殊请求做了优化:

  • HEAD 请求:只发送响应头、不发送正文(send_header_only,L343 与 L389-L390),测试 test_file_response_on_head_method 验证了响应头完整而 body 为空;
  • pathsend 扩展:若 scope 的extensions中声明了http.response.pathsend(ASGI 服务器支持零拷贝发送),则发送http.response.pathsend消息直接交付文件路径(L344 与 L391-L392),避免应用层逐块拷贝。

文件读取以chunk_size = 64 * 1024(64KB)分块进行,且全程通过anyio.open_file异步完成,不阻塞事件循环。

第三方响应:EventSourceResponse(Server-Sent Events)

除内置响应类外,Starlette 生态中还有第三方实现的响应类,典型代表是sse-starlette 提供的EventSourceResponse(第三方包,使用时需单独安装sse-starlette)。它实现了 Server-Sent Events(SSE)协议,让服务器可以向客户端持续推送事件流——与 WebSocket 相比,SSE 基于单向 HTTP 长连接,无需额外协议握手、天然支持重连与事件 ID,适合实时通知、日志推送、AI 流式对话等"服务器单向推送"场景。

从实现角度可以理解为:EventSourceResponse本质上是一个基于StreamingResponse之上的封装,它负责把事件数据按 SSE 规范(data:/event:/id:/retry:行格式)编码成流式分块发送,同时设置正确的Content-Type: text/event-stream与禁用缓冲的相关头。若你的应用已经用到了StreamingResponse的分块机制,再理解 SSE 流式推送就会非常顺理成章。

综合实践:一个整合全部响应能力的路由示例

把文档与源码中的要点组合起来,一个同时展示 JSON、Cookie、重定向、流式与文件响应的应用可以这样组织:

from starlette.responses import ( FileResponse, JSONResponse, RedirectResponse, Response, StreamingResponse, ) async def app(scope, receive, send): assert scope['type'] == 'http' path = scope['path'] if path == '/json': response = JSONResponse({"message": "你好,Starlette"}) # UTF-8 直出,紧凑序列化 response.set_cookie("session", "abc123", max_age=3600, httponly=True, samesite="lax") elif path == '/old': response = RedirectResponse(url='/json') # 默认 307 elif path == '/stream': async def gen(): for i in range(5): yield f"chunk-{i}\n" response = StreamingResponse(gen(), media_type="text/plain") elif path == '/download': response = FileResponse('statics/example.txt', filename='example.txt') # 自动 ETag / Last-Modified / Range else: response = Response('Not Found', status_code=404, media_type='text/plain') await response(scope, receive, send)

可以借助 tests/test_responses.py 中对应的测试(test_redirect_responsetest_set_cookietest_file_responsetest_streaming_response等)来验证各响应类的实际输出头与状态码,从而把文档描述落到可观测的代码行为上。

总结

Starlette 的响应体系围绕"响应即 ASGI 应用"这一核心设计展开:Response基类负责Content-Length/Content-Type的自动填充与 Cookie 操作,HTMLResponse/PlainTextResponse/JSONResponse提供最常用的快捷封装,RedirectResponse以 307 完成安全重定向,StreamingResponse打通异步与同步迭代器的分块传输,而FileResponse则把静态文件服务、ETag 缓存校验、Range 断点续传、多段 multipart 响应与零拷贝发送集于一身。无论你在框架层面使用哪种响应,都可以追溯到 starlette/responses.py 中的统一send协议实现,并用 tests/test_responses.py 中的测试用例作为行为基准。

【免费下载链接】starletteThe little ASGI framework that shines. 🌟项目地址: https://gitcode.com/gh_mirrors/st/starlette

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 9:55:20

3步吃透PSO2底层:从入门到精通的架构拆解

3步吃透PSO2底层:从入门到精通的架构拆解 看了一堆教程还是不会写项目?别急着怪自己笨,大概率是你把“语法”当成了“架构”。在编程领域, 入门到精通 的分水岭,从来不是记住了多少API,而是你能否透过代码表象,看懂数据流动的底层逻辑。 今天咱们不聊那些虚头巴脑的概念,直接撕开 pso2…

作者头像 李华
网站建设 2026/9/23 9:55:19

windows7和vista老项目入门到精通实战避坑指南

windows7和vista老项目入门到精通实战避坑指南 是不是也遇到过这种情况:手头有个老系统,基于windows7和vista开发,文档寥寥无几,看了一堆教程还是不会写项目,代码一跑就报错,配置改了三遍还是连不上数据库。这种“入门到精通”的鸿沟,往往不是卡在语法上,而是卡在环境兼容性和底层逻辑的…

作者头像 李华
网站建设 2026/9/23 9:54:46

冰雷火性能调优保姆级教程:3步解决面试卡顿难题

冰雷火性能调优保姆级教程:3步解决面试卡顿难题 面试时被追问“冰雷火”底层原理,你还能答出个一二三吗?很多应届生在八股文里背得滚瓜烂熟,一旦面试官要求现场手写优化代码,大脑瞬间一片空白。这种“原理懂但手废”的困境,正是技术成长路上的最大绊脚石。今天这篇保姆级教程,不玩虚的,直接拆解【冰雷火】在高性能…

作者头像 李华
网站建设 2026/9/23 9:54:38

2026最新手机充电口坏了故障排查指南

2026最新手机充电口坏了故障排查指南 报错一堆看不懂?StackTrace 满屏飞?别慌,这年头谁没被过。手机充电口坏了,看似硬件问题,实则是系统、驱动、协议三方博弈的深坑。2026最新实测发现,90%的“坏口”其实是软件配置或线材兼容性问题。今天这篇干货,专门给咱们劳务班组负责人这类技术骨干,拆…

作者头像 李华
网站建设 2026/9/23 9:54:32

3分钟搞懂Fusion核心源码速查手册

3分钟搞懂Fusion核心源码速查手册 官方文档堆砌细节让人头大?别急,这份 Fusion源码速查手册 直接带你钻透核心。 作为市政公用工程一线从业者,我们常面临多任务并行:既要考市政工程师证(科目含法规、实务、管理),又要处理现场违规隐患。就像写代码,若不懂框架内核,遇到Bug只能瞎改。…

作者头像 李华
网站建设 2026/9/23 9:54:14

3步搞定直勾玉写轮眼性能优化,告别报错焦虑

3步搞定直勾玉写轮眼性能优化,告别报错焦虑 报错一堆看不懂 StackTrace?别慌,这不仅是代码问题,更是你架构思路的卡点。直勾玉写轮眼这种高并发场景,光靠硬抗肯定不行,得懂 性能优化 。今天这篇干货,专为劳务班组负责人和后端开发准备,把那些晦涩的日志变成你能直接落地的方案。…

作者头像 李华