From 8ca9ffee680e3e75e4ed5d5ea44614e9eabfd5de Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 16 Jul 2025 17:56:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B9=E8=BF=9Bsocket=20hang=20up?= =?UTF-8?q?=E5=92=8C=E7=BD=91=E7=BB=9C=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复socket hang up错误导致返回空字符串的问题 - 改进非流式请求的错误处理,根据错误类型返回适当的HTTP状态码 - 优化流式请求的错误处理,返回SSE格式的错误事件 - 增强错误日志记录,包含详细的网络错误信息 - 确保在任何情况下都返回有效的JSON响应格式 修复内容: - ECONNRESET错误返回502状态码和明确的错误信息 - ENOTFOUND错误返回502状态码和DNS解析失败信息 - ECONNREFUSED错误返回502状态码和连接被拒绝信息 - ETIMEDOUT错误返回504状态码和超时信息 - 流式请求错误时返回符合SSE规范的错误事件 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/api.js | 36 +++++++- src/services/claudeRelayService.js | 137 ++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 18 deletions(-) diff --git a/src/routes/api.js b/src/routes/api.js index f8a2d9d2..140a7d12 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -145,13 +145,41 @@ router.post('/v1/messages', authenticateApiKey, async (req, res) => { logger.api(`✅ Request completed in ${duration}ms for key: ${req.apiKey.name}`); } catch (error) { - logger.error('❌ Claude relay error:', error); + logger.error('❌ Claude relay error:', error.message, { + code: error.code, + stack: error.stack + }); + // 确保在任何情况下都能返回有效的JSON响应 if (!res.headersSent) { - res.status(500).json({ - error: 'Relay service error', - message: error.message + // 根据错误类型设置适当的状态码 + let statusCode = 500; + let errorType = 'Relay service error'; + + if (error.message.includes('Connection reset') || error.message.includes('socket hang up')) { + statusCode = 502; + errorType = 'Upstream connection error'; + } else if (error.message.includes('Connection refused')) { + statusCode = 502; + errorType = 'Upstream service unavailable'; + } else if (error.message.includes('timeout')) { + statusCode = 504; + errorType = 'Upstream timeout'; + } else if (error.message.includes('resolve') || error.message.includes('ENOTFOUND')) { + statusCode = 502; + errorType = 'Upstream hostname resolution failed'; + } + + res.status(statusCode).json({ + error: errorType, + message: error.message || 'An unexpected error occurred', + timestamp: new Date().toISOString() }); + } else { + // 如果响应头已经发送,尝试结束响应 + if (!res.destroyed && !res.finished) { + res.end(); + } } } }); diff --git a/src/services/claudeRelayService.js b/src/services/claudeRelayService.js index 93d4dc16..4c2f2899 100644 --- a/src/services/claudeRelayService.js +++ b/src/services/claudeRelayService.js @@ -281,8 +281,27 @@ class ClaudeRelayService { } req.on('error', (error) => { - logger.error('❌ Claude API request error:', error); - reject(error); + logger.error('❌ Claude API request error:', error.message, { + code: error.code, + errno: error.errno, + syscall: error.syscall, + address: error.address, + port: error.port + }); + + // 根据错误类型提供更具体的错误信息 + let errorMessage = 'Upstream request failed'; + if (error.code === 'ECONNRESET') { + errorMessage = 'Connection reset by Claude API server'; + } else if (error.code === 'ENOTFOUND') { + errorMessage = 'Unable to resolve Claude API hostname'; + } else if (error.code === 'ECONNREFUSED') { + errorMessage = 'Connection refused by Claude API server'; + } else if (error.code === 'ETIMEDOUT') { + errorMessage = 'Connection timed out to Claude API server'; + } + + reject(new Error(errorMessage)); }); req.on('timeout', () => { @@ -453,12 +472,46 @@ class ClaudeRelayService { }); req.on('error', (error) => { - logger.error('❌ Claude stream request error:', error); - if (!responseStream.headersSent) { - responseStream.writeHead(500, { 'Content-Type': 'application/json' }); + logger.error('❌ Claude stream request error:', error.message, { + code: error.code, + errno: error.errno, + syscall: error.syscall + }); + + // 根据错误类型提供更具体的错误信息 + let errorMessage = 'Upstream request failed'; + let statusCode = 500; + if (error.code === 'ECONNRESET') { + errorMessage = 'Connection reset by Claude API server'; + statusCode = 502; + } else if (error.code === 'ENOTFOUND') { + errorMessage = 'Unable to resolve Claude API hostname'; + statusCode = 502; + } else if (error.code === 'ECONNREFUSED') { + errorMessage = 'Connection refused by Claude API server'; + statusCode = 502; + } else if (error.code === 'ETIMEDOUT') { + errorMessage = 'Connection timed out to Claude API server'; + statusCode = 504; } + + if (!responseStream.headersSent) { + responseStream.writeHead(statusCode, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + } + if (!responseStream.destroyed) { - responseStream.end(JSON.stringify({ error: 'Upstream request failed' })); + // 发送 SSE 错误事件 + responseStream.write(`event: error\n`); + responseStream.write(`data: ${JSON.stringify({ + error: errorMessage, + code: error.code, + timestamp: new Date().toISOString() + })}\n\n`); + responseStream.end(); } reject(error); }); @@ -467,10 +520,21 @@ class ClaudeRelayService { req.destroy(); logger.error('❌ Claude stream request timeout'); if (!responseStream.headersSent) { - responseStream.writeHead(504, { 'Content-Type': 'application/json' }); + responseStream.writeHead(504, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); } if (!responseStream.destroyed) { - responseStream.end(JSON.stringify({ error: 'Request timeout' })); + // 发送 SSE 错误事件 + responseStream.write(`event: error\n`); + responseStream.write(`data: ${JSON.stringify({ + error: 'Request timeout', + code: 'TIMEOUT', + timestamp: new Date().toISOString() + })}\n\n`); + responseStream.end(); } reject(new Error('Request timeout')); }); @@ -538,12 +602,46 @@ class ClaudeRelayService { }); req.on('error', (error) => { - logger.error('❌ Claude stream request error:', error); - if (!responseStream.headersSent) { - responseStream.writeHead(500, { 'Content-Type': 'application/json' }); + logger.error('❌ Claude stream request error:', error.message, { + code: error.code, + errno: error.errno, + syscall: error.syscall + }); + + // 根据错误类型提供更具体的错误信息 + let errorMessage = 'Upstream request failed'; + let statusCode = 500; + if (error.code === 'ECONNRESET') { + errorMessage = 'Connection reset by Claude API server'; + statusCode = 502; + } else if (error.code === 'ENOTFOUND') { + errorMessage = 'Unable to resolve Claude API hostname'; + statusCode = 502; + } else if (error.code === 'ECONNREFUSED') { + errorMessage = 'Connection refused by Claude API server'; + statusCode = 502; + } else if (error.code === 'ETIMEDOUT') { + errorMessage = 'Connection timed out to Claude API server'; + statusCode = 504; } + + if (!responseStream.headersSent) { + responseStream.writeHead(statusCode, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); + } + if (!responseStream.destroyed) { - responseStream.end(JSON.stringify({ error: 'Upstream request failed' })); + // 发送 SSE 错误事件 + responseStream.write(`event: error\n`); + responseStream.write(`data: ${JSON.stringify({ + error: errorMessage, + code: error.code, + timestamp: new Date().toISOString() + })}\n\n`); + responseStream.end(); } reject(error); }); @@ -552,10 +650,21 @@ class ClaudeRelayService { req.destroy(); logger.error('❌ Claude stream request timeout'); if (!responseStream.headersSent) { - responseStream.writeHead(504, { 'Content-Type': 'application/json' }); + responseStream.writeHead(504, { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive' + }); } if (!responseStream.destroyed) { - responseStream.end(JSON.stringify({ error: 'Request timeout' })); + // 发送 SSE 错误事件 + responseStream.write(`event: error\n`); + responseStream.write(`data: ${JSON.stringify({ + error: 'Request timeout', + code: 'TIMEOUT', + timestamp: new Date().toISOString() + })}\n\n`); + responseStream.end(); } reject(new Error('Request timeout')); });