issue: upstream failure mid-stream truncates /api/chat/completions SSE with no error frame or [DONE]

当 OpenAI 兼容上游在 Open WebUI 已经转发 200 响应头之后断开连接时, /api/chat/completions 的 SSE 流会被静默截断——没有错误帧、没有 [DONE] ,客户端往往把它当成“正常结束的空回复”。优先排查上游(如 LiteLLM、Bedrock)是否在流

快速结论:当 OpenAI 兼容上游在 Open WebUI 已经转发 200 响应头之后断开连接时,/api/chat/completions 的 SSE 流会被静默截断——没有错误帧、没有 [DONE],客户端往往把它当成“正常结束的空回复”。优先排查上游(如 LiteLLM、Bedrock)是否在流中途断开,以及 Open WebUI 是否已包含对该透传路径的修复。

适用环境:Open WebUI dev @ 18a48cf;同一问题也在 v0.9.6 生产环境复现(Open WebUI → LiteLLM 1.98.0 → Bedrock)。操作系统:Ubuntu 24.04。安装方式:Git Clone。Issue 未提供 Python、CUDA、显卡、Ollama 版本信息。

最快修复方案:暂无确认的一步修复方案。该 Issue 标记为 confirmed issue,指向的修复 PR 为 open-webui/open-webui #29697,但在本地验证前应视为“可优先尝试”:升级到包含该 PR 的版本,或按 PR 思路在 stream_wrapper 捕获上游异常后,先写一个空行终止被截断的未完成事件,再补发错误帧与 [DONE]

注意事项:Open WebUI 已确认:该透传路径转发的是原始网络 chunk,而不是完整行,因此失败前最后一个 chunk 可能停在事件中间;若直接把错误帧拼上去,会把错误帧粘在半行后面,产生格式错误的 SSE 事件。因此必须先补空行再发错误帧。另外,这只是在客户端侧“暴露”上游失败,并不修复上游断流本身;Issue 中该补丁尚未合并时,不要让任何命令或配置被当作已验证结论。

问题场景

用户在 Open WebUI 中配置了一个 OpenAI 兼容连接,请求经过 /api/chat/completions 透传,上游(例如 LiteLLM 1.98.0 后面的 Bedrock)在 Open WebUI 已经返回 200 响应头之后中断了流式响应。API 客户端收到的是一个突然停止的 200 event stream,而不是带错误的响应或正常的 [DONE] 结束标记。在实际部署中,这表现为三天内 156 次静默的空补全,客户端日志里看不到真正的上游错误。

报错原文

issue: upstream failure mid-stream truncates /api/chat/completions SSE with no error frame or [DONE]

data: {"choices":[{"delta":{"content":"1"}}]}

data: {"choices":[{"delta":{"content":"2"}}]}

curl: (18) transfer closed with outstanding read data remaining

--- curl exit=18 http=200 bytes=94

ClientPayloadError: Response payload is not completed: 

原因分析

可能原因是:上游在流式响应中途断开后,aiohttp 抛出 ClientPayloadErrorServerDisconnectedError 或空闲 TimeoutError,而 backend/open_webui/utils/session_pool.py 中的 stream_wrapper 没有捕获这些异常。此时 Starlette 已经无法更改 HTTP 状态码,只能直接断开连接,于是客户端拿到一个 200 的事件流,但流中既没有错误帧,也没有 data: [DONE]。多数 OpenAI SDK 会把这种“正常结束但内容为空”的流记录为一次成功的空补全,导致上游错误在客户端侧不可见。

Issue 讨论中已确认:这是 /api/chat/completions 透传路径的问题,与聊天 UI 路径(process_chat 会捕获 TransferEncodingError 并展示给用户,如 #24559)不同;与 #13474 的 chunk 边界解析问题不同(后者在 dev 上已由 stream_chunks_handler 处理);与 #26776 无关。

环境排查

  • 确认 Open WebUI 版本与安装方式:Issue 复现于 dev @ 18a48cfv0.9.6,安装方式为 Git Clone。
  • 确认上游链路:Open WebUI → LiteLLM 1.98.0 → Bedrock 是 Issue 中复现的生产链路。
  • 确认操作系统:Ubuntu 24.04。
  • 确认 backend/open_webui/utils/session_pool.pystream_wrapper 是否捕获 aiohttp 的 ClientPayloadErrorServerDisconnectedErrorTimeoutError
  • 确认透传路径是否直接转发原始网络 chunk(而非按行转发),这会影响错误帧的插入方式。
  • Issue 未提供 Python、CUDA、PyTorch、显卡、Ollama 版本信息,这些项目无需作为本问题的必要排查项。

解决步骤

  1. 先区分问题路径:确认客户端走的是 /api/chat/completions 透传,而不是聊天 UI 的 process_chat 路径。如果是聊天 UI 路径,参考 #24559 的现有处理方式。
  2. 用 Issue 中的复现方式确认问题:启动一个会在写入两个 SSE chunk 后调用 request.transport.abort() 的假上游,再通过 curl -sS -N -i -X POST .../api/chat/completions 请求,观察是否出现截断、无 [DONE]curl 退出码 18。
  3. stream_wrapper 中捕获上游异常(aiohttp 的 ClientPayloadErrorServerDisconnectedError、空闲 TimeoutError)。
  4. 捕获后不要直接追加错误帧:先写入一个空行,把失败前可能停在半行的事件终止掉,避免错误帧与未完成 chunk 粘连成格式错误的事件。
  5. 随后写入 data: {"error": {...}} 错误帧,再写入 data: [DONE],并以正常方式结束 chunked body。
  6. 如果不想自行改动,可优先尝试升级到包含 PR #29697 的版本;该 PR 尚未在 Issue 中被标记为合并验证。

验证方法

用同一条 curl 命令重新请求 /api/chat/completions:如果修复生效,上游中途断开时客户端应收到错误帧和 data: [DONE]curl 不再报 (18) transfer closed with outstanding read data remaining,也不再出现“200 但流突然停止”的情况。同时检查 API 客户端日志,确认此前被记录为成功空补全的请求现在能被识别为错误。

参考来源

open-webui/open-webui #29628

open-webui/open-webui PR #29697

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 23167

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注