Starlette SSE 已发 response.start,客户端为什么还收不到响应头
旧 GZipMiddleware 会等待首块正文才转发本可直接发送的 SSE 响应头。Starlette 1.8.0 已调整这条路径;核对框架依赖约束与完整代理链后再升级或分流。
SSE 应用已经发出 http.response.start,压缩中间件内侧的日志也看到了 200,外侧却还没有状态码,客户端一直等到第一条事件出现。这时需要检查应用内的 GZipMiddleware,不能只调整反向代理的缓冲参数。
旧实现会先保存响应头,等第一块正文到达后再决定如何转发。即使响应是已经排除压缩的 text/event-stream,也可能经过这段等待。流在首个正文之前断开时,外层监控甚至可能始终没看到响应开始。
先确认延迟发生在哪一层
保留 Starlette 版本、中间件顺序、响应的 Content-Type,以及应用内外两侧观察到的 ASGI 消息时刻。检查真实 GET 流,不用 HEAD 请求或一个已经结束的普通响应代替 SSE。
对同一个获准的诊断端点,比较经过 GZipMiddleware 和不经过它的路径;保持认证、请求参数与事件产生时刻一致。要分别记录应用发送 start、首块 body、服务器发出响应头以及客户端收到事件的时刻。
如果应用本来就要等到第一条数据才发送 http.response.start,中间件不可能替它提前建立响应。若 start 已及时转发,延迟还在后面,再查 ASGI 服务器、其他中间件、反向代理或客户端。ASGI 消息转发及时,也不单独保证整条网络链立即交付。
Caddy 编码层曾有相似的等待,见 Caddy 开启 gzip 后 SSE 迟迟没有响应。那是另一个位置的处理;绕过代理后仍有问题时,不能继续只改 Caddy。
1.8.0 已调整不需要压缩的响应
Starlette 1.8.0 会对已有 Content-Encoding、状态为 206,或命中排除类型的响应,立即向下游转发 http.response.start,不再等待第一块正文。1.9.0 正式实现也保留这项处理。
对 SSE 而言,确认 Content-Type 真的是 text/event-stream,而非普通 JSON、HTML 错误页或某个未排除的自定义类型。普通需要压缩的响应仍可能等待正文来决定头字段,不能把这次改动解释成所有响应都立即发送头。
升级还涉及兼容范围:Starlette 1.8.0 要求 Python 3.11 及以上。通过 FastAPI 等框架间接使用 Starlette 时,应先核对上层框架声明的版本约束;不要强装一个不在范围内的 Starlette,再忽略依赖冲突。
保留原锁文件、运行镜像与中间件配置,在项目支持的组合中升级,再用原流端点复查。版本号改变但运行中的旧进程没有替换,也不会改变请求路径。
立即转发响应开始也有取舍:如果应用在首块正文之前抛错,先前的 200 已经提交,外层错误处理不能再把它改成 500,连接可能直接结束。升级后要保留并观察这一类服务端异常,不能只凭客户端先收到 200 就认定流已经成功建立。
暂时不能升级时,按应用路由避开压缩层
由自己控制应用组装时,可以让已知 SSE 路由进入不包裹 GZipMiddleware 的应用分支,普通响应继续使用压缩。改的是应用内的中间件覆盖范围,不是另开一个没有认证的公开端口。
调整前画清现有中间件顺序:认证、CORS、错误处理和外层日志仍应覆盖原来的请求。若 GZipMiddleware 继续包在最外面,只在内部子应用删掉一次压缩配置,SSE 仍会经过外层等待路径。
不要把 minimum_size 改小或改成零当成这条故障的解决办法。旧路径是在首块 body 之前等待,尚没有正文可供长度判断;也不需要为了让头早点出现而伪造业务事件。
验收覆盖首条事件长时间不出现、事件间隔发送、客户端在首块正文前断开、应用在 start 之后首块正文之前抛错,以及普通 JSON、已编码响应和范围响应。确认响应头经过正确路径、事件边界完整、外层日志能记录实际状态,认证与 CORS 没有因分流改变。若升级或路由调整引入问题,恢复整套依赖与中间件配置,保留分层时序记录继续定位。


