From 210eba4a46c5e1dae3fac70db719a21f209e3eef Mon Sep 17 00:00:00 2001 From: Xiaoyong Date: Fri, 27 Feb 2026 11:16:42 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A7=A3=E5=8E=8B=20streaming=20?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=9A=84=20gzip=20=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=20SSE=20=E6=95=B0=E6=8D=AE=E6=8D=9F?= =?UTF-8?q?=E5=9D=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic API (经 Cloudflare) 返回 Content-Encoding: gzip 的压缩响应。 非 streaming 处理器已有 zlib.gunzipSync 解压,但 streaming 处理器 直接对 gzip 二进制数据调用 chunk.toString(),导致非法 UTF-8 字节 被替换为 U+FFFD,SSE 数据损坏。 修复:在 streaming 的 res.on('data') 之前检测 content-encoding, 如为 gzip/deflate 则通过 zlib 管道解压后再处理。 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/services/relay/claudeRelayService.js | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/services/relay/claudeRelayService.js b/src/services/relay/claudeRelayService.js index 757fb09f..a0062514 100644 --- a/src/services/relay/claudeRelayService.js +++ b/src/services/relay/claudeRelayService.js @@ -2386,7 +2386,28 @@ class ClaudeRelayService { const requestedModel = body?.model || 'unknown' const { isRealClaudeCodeRequest } = requestOptions - res.on('data', (chunk) => { + // 🔧 处理上游 gzip/deflate 压缩:Anthropic (经 Cloudflare) 可能返回压缩响应 + const upstreamEncoding = res.headers['content-encoding'] + let dataSource = res + if (upstreamEncoding === 'gzip') { + dataSource = res.pipe(zlib.createGunzip()) + dataSource.on('error', (err) => { + logger.error('❌ Gzip decompression error in stream:', err.message) + if (isStreamWritable(responseStream)) { + responseStream.end() + } + }) + } else if (upstreamEncoding === 'deflate') { + dataSource = res.pipe(zlib.createInflate()) + dataSource.on('error', (err) => { + logger.error('❌ Deflate decompression error in stream:', err.message) + if (isStreamWritable(responseStream)) { + responseStream.end() + } + }) + } + + dataSource.on('data', (chunk) => { try { const chunkStr = chunk.toString() @@ -2531,7 +2552,7 @@ class ClaudeRelayService { } }) - res.on('end', async () => { + dataSource.on('end', async () => { try { // 处理缓冲区中剩余的数据 if (buffer.trim() && isStreamWritable(responseStream)) {