Files
claude-relay-service/src/utils/errorSanitizer.js
root 24f825f60d style: format all files with prettier
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 15:05:58 +08:00

263 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 错误消息清理工具 - 白名单错误码制
* 所有错误映射到预定义的标准错误码,原始消息只记日志不返回前端
*/
const logger = require('./logger')
// 标准错误码定义
const ERROR_CODES = {
E001: { message: 'Service temporarily unavailable', status: 503 },
E002: { message: 'Network connection failed', status: 502 },
E003: { message: 'Authentication failed', status: 401 },
E004: { message: 'Rate limit exceeded', status: 429 },
E005: { message: 'Invalid request', status: 400 },
E006: { message: 'Model not available', status: 503 },
E007: { message: 'Upstream service error', status: 502 },
E008: { message: 'Request timeout', status: 504 },
E009: { message: 'Permission denied', status: 403 },
E010: { message: 'Resource not found', status: 404 },
E011: { message: 'Account temporarily unavailable', status: 503 },
E012: { message: 'Server overloaded', status: 529 },
E013: { message: 'Invalid API key', status: 401 },
E014: { message: 'Quota exceeded', status: 429 },
E015: { message: 'Internal server error', status: 500 }
}
// 错误特征匹配规则(按优先级排序)
const ERROR_MATCHERS = [
// 网络层错误
{ pattern: /ENOTFOUND|DNS|getaddrinfo/i, code: 'E002' },
{ pattern: /ECONNREFUSED|ECONNRESET|connection refused/i, code: 'E002' },
{ pattern: /ETIMEDOUT|timeout/i, code: 'E008' },
{ pattern: /ECONNABORTED|aborted/i, code: 'E002' },
// 认证错误
{ pattern: /unauthorized|invalid.*token|token.*invalid|invalid.*key/i, code: 'E003' },
{ pattern: /invalid.*api.*key|api.*key.*invalid/i, code: 'E013' },
{ pattern: /authentication|auth.*fail/i, code: 'E003' },
// 权限错误
{ pattern: /forbidden|permission.*denied|access.*denied/i, code: 'E009' },
{ pattern: /does not have.*permission/i, code: 'E009' },
// 限流错误
{ pattern: /rate.*limit|too many requests|429/i, code: 'E004' },
{ pattern: /quota.*exceeded|usage.*limit/i, code: 'E014' },
// 过载错误
{ pattern: /overloaded|529|capacity/i, code: 'E012' },
// 账户错误
{ pattern: /account.*disabled|organization.*disabled/i, code: 'E011' },
{ pattern: /too many active sessions/i, code: 'E011' },
// 模型错误
{ pattern: /model.*not.*found|model.*unavailable|unsupported.*model/i, code: 'E006' },
// 请求错误
{ pattern: /bad.*request|invalid.*request|malformed/i, code: 'E005' },
{ pattern: /not.*found|404/i, code: 'E010' },
// 上游错误
{ pattern: /upstream|502|bad.*gateway/i, code: 'E007' },
{ pattern: /503|service.*unavailable/i, code: 'E001' }
]
/**
* 根据原始错误匹配标准错误码
* @param {Error|string|object} error - 原始错误
* @param {object} options - 选项
* @param {string} options.context - 错误上下文(用于日志)
* @param {boolean} options.logOriginal - 是否记录原始错误默认true
* @returns {{ code: string, message: string, status: number }}
*/
function mapToErrorCode(error, options = {}) {
const { context = 'unknown', logOriginal = true } = options
// 提取原始错误信息
const originalMessage = extractOriginalMessage(error)
const errorCode = error?.code || error?.response?.status
const statusCode = error?.response?.status || error?.status || error?.statusCode
// 记录原始错误到日志(供调试)
if (logOriginal && originalMessage) {
logger.debug(`[ErrorSanitizer] Original error (${context}):`, {
message: originalMessage,
code: errorCode,
status: statusCode
})
}
// 匹配错误码
let matchedCode = 'E015' // 默认:内部服务器错误
// 先按 HTTP 状态码快速匹配
if (statusCode) {
if (statusCode === 401) {
matchedCode = 'E003'
} else if (statusCode === 403) {
matchedCode = 'E009'
} else if (statusCode === 404) {
matchedCode = 'E010'
} else if (statusCode === 429) {
matchedCode = 'E004'
} else if (statusCode === 502) {
matchedCode = 'E007'
} else if (statusCode === 503) {
matchedCode = 'E001'
} else if (statusCode === 504) {
matchedCode = 'E008'
} else if (statusCode === 529) {
matchedCode = 'E012'
}
}
// 再按消息内容精确匹配(可能覆盖状态码匹配)
if (originalMessage) {
for (const matcher of ERROR_MATCHERS) {
if (matcher.pattern.test(originalMessage)) {
matchedCode = matcher.code
break
}
}
}
// 按错误 code 匹配(网络错误)
if (errorCode) {
const codeStr = String(errorCode).toUpperCase()
if (codeStr === 'ENOTFOUND' || codeStr === 'EAI_AGAIN') {
matchedCode = 'E002'
} else if (codeStr === 'ECONNREFUSED' || codeStr === 'ECONNRESET') {
matchedCode = 'E002'
} else if (codeStr === 'ETIMEDOUT' || codeStr === 'ESOCKETTIMEDOUT') {
matchedCode = 'E008'
} else if (codeStr === 'ECONNABORTED') {
matchedCode = 'E002'
}
}
const result = ERROR_CODES[matchedCode]
return {
code: matchedCode,
message: result.message,
status: result.status
}
}
/**
* 提取原始错误消息
*/
function extractOriginalMessage(error) {
if (!error) {
return ''
}
if (typeof error === 'string') {
return error
}
if (error.message) {
return error.message
}
if (error.response?.data?.error?.message) {
return error.response.data.error.message
}
if (error.response?.data?.error) {
return String(error.response.data.error)
}
if (error.response?.data?.message) {
return error.response.data.message
}
return ''
}
/**
* 创建安全的错误响应对象
* @param {Error|string|object} error - 原始错误
* @param {object} options - 选项
* @returns {{ error: { code: string, message: string }, status: number }}
*/
function createSafeErrorResponse(error, options = {}) {
const mapped = mapToErrorCode(error, options)
return {
error: {
code: mapped.code,
message: mapped.message
},
status: mapped.status
}
}
/**
* 创建安全的 SSE 错误事件
* @param {Error|string|object} error - 原始错误
* @param {object} options - 选项
* @returns {string} - SSE 格式的错误事件
*/
function createSafeSSEError(error, options = {}) {
const mapped = mapToErrorCode(error, options)
return `event: error\ndata: ${JSON.stringify({
error: mapped.message,
code: mapped.code,
timestamp: new Date().toISOString()
})}\n\n`
}
/**
* 获取安全的错误消息(用于替换 error.message
* @param {Error|string|object} error - 原始错误
* @param {object} options - 选项
* @returns {string}
*/
function getSafeMessage(error, options = {}) {
return mapToErrorCode(error, options).message
}
// 兼容旧接口
function sanitizeErrorMessage(message) {
if (!message) {
return 'Service temporarily unavailable'
}
return mapToErrorCode({ message }, { logOriginal: false }).message
}
function sanitizeUpstreamError(errorData) {
return createSafeErrorResponse(errorData, { logOriginal: false })
}
function extractErrorMessage(body) {
return extractOriginalMessage(body)
}
function isAccountDisabledError(statusCode, body) {
if (statusCode !== 400) {
return false
}
const message = extractOriginalMessage(body)
if (!message) {
return false
}
const lower = message.toLowerCase()
return (
lower.includes('organization has been disabled') ||
lower.includes('account has been disabled') ||
lower.includes('account is disabled') ||
lower.includes('no account supporting') ||
lower.includes('account not found') ||
lower.includes('invalid account') ||
lower.includes('too many active sessions')
)
}
module.exports = {
ERROR_CODES,
mapToErrorCode,
createSafeErrorResponse,
createSafeSSEError,
getSafeMessage,
// 兼容旧接口
sanitizeErrorMessage,
sanitizeUpstreamError,
extractErrorMessage,
isAccountDisabledError
}