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')); });