乐于分享
好东西不私藏

OpenClaw 升级后 SSE 流式报 Connection error?缺失 Content-Type 排查实录

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

教训

  1. 升级 AI 工具链要特别小心。OpenClaw 6.1 的 Content-Type 严格检查是静默引入的,不会写在 release notes 的显眼位置。升级后一定要测流式请求。

  2. SSE 流的 Content-Type 不是可选的。很多服务端框架在返回 StreamingResponseBody 时不会自动加 Content-Type,需要手动设置。客户端(尤其是新版 OpenClaw、Claude Code 等)会严格检查这个头。

  3. 不要假设 “之前能用 = 代码没问题”。我们的透传模式 bug 从 4月22日就存在,只是恰好之前的客户端版本够宽容。

  4. 日志是最诚实的证人。OpenClaw 的 contentType= 空值直接锁定了问题。与其猜测,不如先看日志。


本文基于真实排查过程整理,涉及的项目为自建 AI 聚合网关 + OpenClaw 本地部署。