Caddy 开启 gzip 后 SSE 迟迟没有响应,检查压缩层
SSE直连正常而开启Caddy编码后响应头或事件迟到时,分开检查编码器和代理刷新。核对历史版本、正式处理路径及受控端点时序,必要时只排除已知流路径。
SSE 直连应用时消息及时到达,经过 Caddy 并启用 gzip 或 zstd 后,客户端却迟迟收不到响应头,或一次收到多条事件。即使反向代理已经刷新输出,外面的编码层仍可能没有把压缩帧送给客户端,应把压缩与代理两层分开检查。
Caddy 2.7.6、2.8.0-beta.1 的这一历史问题涉及响应头等待首批正文,以及编码器刷新。正式版本 2.11.7 已有 SSE 响应头立即处理和编码器 flush 路径。如果正在使用该版本,不能直接认定仍是同一个旧缺陷,要检查应用和完整链路。
同一身份、同一路径比较编码开关
使用原来失败的受控 SSE 端点,分别比较直连应用和代理入口。保持身份、请求参数和事件内容相同,确认得到 text/event-stream,没有把登录页或重定向当成事件流。
在现有诊断环境中,可用以下命令观察启用压缩的请求:
curl -N --compressed --max-time 30 \
'https://events.example.com/events'
地址是占位值,接口需要认证时沿用安全的既有认证方式,不把令牌直接贴进共享日志。再把同一个请求改为 Accept-Encoding: identity 比较;-N 关闭 curl 自身的输出缓冲,--compressed 请求并解码受支持的压缩内容。
30 秒到期会主动结束连接,所以该次超时不能单独证明服务出错。记录响应头到达和完整事件出现的时刻,核对业务事件末尾是否有实际空行分隔。仅收到一段压缩数据,或只看 HEAD 请求,无法判断浏览器能否及时得到完整事件。
区分首个响应头与后续事件
应用可能先发送 200 和 text/event-stream,过一段时间才产生正文。旧编码实现等待足够正文再决定压缩,会让客户端连事件流的响应头也等不到。
另一处等待可能发生在编码器内部:应用写入并 flush 一个完整事件,压缩算法仍没有输出完整帧。只刷新底层 ResponseWriter,不能保证编码器里的数据已经送出。这解释了为什么调整代理 flush 后,事件仍可能被积压。
若关闭压缩后表现没有变化,继续检查应用是否实际 flush、事件格式、其他代理或 CDN,以及客户端回调。代理无法提前发送应用尚未交出的数据,也不能补全缺少结束空行的事件。
旧版本升级或只避开已确认的流路径
核对真正运行的 Caddy 二进制、插件和有效配置,保存升级与配置回退入口。在兼容条件允许时,采用具有上述处理的正式版本,例如 2.11.7,并用原端点复查。这里没有把补丁合入日期当成首个修复版本,也不保证升级解决所有事件延迟。
暂时不能升级时,可以将已确认的 SSE 路径排除在 encode 之外。下面展示一个已知事件路径的请求匹配片段:
@ordinary not path /events
encode @ordinary gzip
它应替换对应范围的旧 encode 配置,并与已有路由合并,不能在另一层仍保留一个覆盖该路径的 encode。实际端点有前缀、多个路径或其他路由结构时,应按有效配置调整。鉴权和原来的代理处理必须保留,不能为绕过压缩额外建立一个公开直连入口。
这只是按已知路径避开编码,不是依据响应类型自动识别所有 SSE。也不需要因此关闭整个站点的压缩。当前 Caddy 支持正常的 SSE 编码与刷新,不能把“SSE 永远不该压缩”作为通用结论。
不用长度阈值替代流式处理
将 minimum_length 调成 0 不是关闭门槛的约定,它会采用默认值。给普通小响应设置正数阈值,与让 SSE 正确发送响应头和压缩帧是两个问题。
也不要无条件增加 flush_interval -1。当前代理对 SSE 有自己的刷新判断,额外改变 flush 设置还涉及客户端断开后的处理行为,应按所用版本单独评估,不能用它掩盖编码层等待。
验证要覆盖首个事件之前的连接建立、多个间隔事件、空闲期间心跳和正常断开。同时检查普通响应、编码协商与缓存变体没有回归。若路径调整或升级造成异常,恢复原版本与配套配置,保留已收集的时序证据继续定位。


