OpenClaw 升级后 SSE 流式报 Connection error?缺失 Content-Type 排查实录
升级 OpenClaw 到 2026.6.1 后,所有模型的 SSE 流式请求全部报 “Connection error”。HTTP 200、body 正确——但就是不通。排查了网关代码、路由逻辑、JWT 认证……最后发现,罪魁祸首是一个缺失的
Content-Type: text/event-stream响应头。这篇文章记录了完整的排查过程、根因分析和解决思路,希望帮你避开同样的坑。
正文
故事的开端
某天升级 OpenClaw 到 2026.6.1 版本后,发现 AI 助手完全不工作了。不管用什么模型——kimi-k2.6、MiniMax-M3、MiniMax-M2.7——全部报同一个错:
Something went wrong
TUI 界面上就这么一句话,什么信息都没有。
第一反应:是不是网关挂了?网络问题?API Key 过期了?
第一步:确认网络和认证
先 curl 一下非流式请求:
curl -s https://hqai-gw.e-hqins.com/openai/v1/chat/completions \
-H "Authorization: Bearer sk-xxx" \
-H "Content-Type: application/json" \
-d '{"model":"kimi-k2.6","messages":[{"role":"user","content":"hi"}],"stream":false}'
返回 HTTP 200,Content-Type: application/json,body 完全正确。
认证没问题,网络没问题,非流式正常。
第二步:测试流式请求
curl -si https://hqai-gw.e-hqins.com/openai/v1/chat/completions \
-H "Authorization: Bearer sk-xxx" \
-H "Content-Type: application/json" \
-d '{"model":"kimi-k2.6","messages":[{"role":"user","content":"hi"}],"stream":true}'
结果:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk",...}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk",...}
data: [DONE]
HTTP 200,SSE body 格式完全正确。但是——响应头里没有 Content-Type!
这就是问题的线索。
第三步:看 OpenClaw 日志
OpenClaw 的 gateway 是 systemd 用户服务,日志在 journalctl 里:
journalctl --user -u openclaw-gateway --since "2026-06-10 12:00" | grep -iE "error|fail|content-type|sse|kimi|connection"
关键日志:
[model-fetch] response provider=hqins-prd model=kimi-k2.6 status=200 contentType=
[agent/embedded] error=LLM request failed: network connection error. rawError=Connection error.
[model-fallback/decision] candidate_failed reason=timeout detail=Connection error.
注意 contentType= 后面是空的。 OpenClaw 拿到 HTTP 200,但因为没有 Content-Type,无法识别 SSE 流,直接报 Connection error.。
然后 OpenClaw 的 failover 机制开始依次尝试备用模型:kimi-k2.6 → MiniMax-M3 → MiniMax-M2.7-highspeed → UAT 的 MiniMax-M2.7-highspeed——全部因为同样的原因失败。
最终报错:
All models failed (4): Connection error. (timeout)
第四步:定位代码根因
我们的 AI 中台有透传模式(Passthrough),当入口协议和供应商协议一致时,请求体/响应原样透传上游。
看了控制器代码,问题一目了然:
// OpenAiCompatibleController.java — BUG
ResponseEntity<?> passthrough = llmPassthroughService.tryPassthroughOrNull(...);
if (passthrough != null) {
StreamingResponseBody streamingBody = (StreamingResponseBody) passthrough.getBody();
return streamingBody; // ← 只返回了 body,丢掉了 ResponseEntity 里的 headers!
}
LlmPassthroughService.executeStreaming() 在构建 ResponseEntity 时确实设了 Content-Type:
return ResponseEntity.status(code)
.contentType(contentType) // text/event-stream
.body(streaming);
但控制器把 body 从 ResponseEntity 里剥出来直接返回,Content-Type header 就丢了。
这个 bug 从透传模式上线第一天(4月22日)就存在,只是之前 OpenClaw 版本对 Content-Type 不做严格检查,所以一直没暴露。
第五步:搜类似场景
在 KKClaw(OpenClaw 的桌面分支)的社区里搜索发现,这是 2026.6.1 版本引入的已知回归:新版本在 provider-transport-fetch.ts 里新增了 assertOpenAISdkStreamContentType 检查,要求 SSE 流必须有 Content-Type: text/event-stream。官方 fix 目前只豁免了 openai-chatgpt-responses API,custom provider(如我们的 openai-completions)暂时不在豁免范围内,需要等后续版本或自行 patch。
解决方案
治本——修网关:
// OpenAiCompatibleController.java — FIX
ResponseEntity<?> passthrough = llmPassthroughService.tryPassthroughOrNull(...);
if (passthrough != null) {
return passthrough; // ← 返回完整 ResponseEntity,保留 Content-Type
}
同时 LlmPassthroughService 新增 resolveStreamingResponseMediaType 方法,当上游不返回 Content-Type 时兜底为 text/event-stream:
private static MediaType resolveStreamingResponseMediaType(String headerValue) {
if (headerValue == null || headerValue.isBlank()) {
return MediaType.TEXT_EVENT_STREAM; // 兜底
}
try {
return MediaType.parseMediaType(headerValue);
} catch (Exception e) {
return MediaType.TEXT_EVENT_STREAM;
}
}
两层配合:Service 层确保 ResponseEntity 包含正确的 Content-Type,Controller 层完整返回不拆 body。
治标——OpenClaw 侧:
如果没法立即修网关,可以在 OpenClaw 配置中加 tolerance,但目前官方只对 openai-chatgpt-responses 开了口子,custom provider 需要等官方后续版本修复。
排查思路总结
| 步骤 | 做了什么 | 发现 |
|---|---|---|
| 1 | curl 非流式请求 | 200 OK,正常 |
| 2 | curl 流式请求(-i 看 header) |
200 OK,body 正常,无 Content-Type |
| 3 | journalctl 看 OpenClaw 日志 | contentType= 空,Connection error. |
| 4 | 读控制器代码 | return streamingBody 丢掉了 headers |
教训
-
升级 AI 工具链要特别小心。OpenClaw 6.1 的 Content-Type 严格检查是静默引入的,不会写在 release notes 的显眼位置。升级后一定要测流式请求。
-
SSE 流的 Content-Type 不是可选的。很多服务端框架在返回
StreamingResponseBody时不会自动加 Content-Type,需要手动设置。客户端(尤其是新版 OpenClaw、Claude Code 等)会严格检查这个头。 -
不要假设 “之前能用 = 代码没问题”。我们的透传模式 bug 从 4月22日就存在,只是恰好之前的客户端版本够宽容。
-
日志是最诚实的证人。OpenClaw 的
contentType=空值直接锁定了问题。与其猜测,不如先看日志。
本文基于真实排查过程整理,涉及的项目为自建 AI 聚合网关 + OpenClaw 本地部署。
夜雨聆风