error-handler.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. import {
  2. isClaudeErrorFormat,
  3. isGeminiErrorFormat,
  4. isOpenAIErrorFormat,
  5. isValidErrorOverrideResponse,
  6. } from "@/lib/error-override-validator";
  7. import { logger } from "@/lib/logger";
  8. import { ProxyStatusTracker } from "@/lib/proxy-status-tracker";
  9. import { updateMessageRequestDetails, updateMessageRequestDuration } from "@/repository/message";
  10. import {
  11. getErrorOverrideAsync,
  12. isEmptyResponseError,
  13. isRateLimitError,
  14. ProxyError,
  15. type RateLimitError,
  16. } from "./errors";
  17. import { ProxyResponses } from "./responses";
  18. import type { ProxySession } from "./session";
  19. /** 覆写状态码最小值 */
  20. const OVERRIDE_STATUS_CODE_MIN = 400;
  21. /** 覆写状态码最大值 */
  22. const OVERRIDE_STATUS_CODE_MAX = 599;
  23. /**
  24. * 根据限流类型计算 HTTP 状态码
  25. * - RPM/并发用 429 Too Many Requests(可重试的频率控制)
  26. * - 消费限额用 402 Payment Required(需充值/等待重置)
  27. */
  28. function getRateLimitStatusCode(limitType: string): number {
  29. return limitType === "rpm" || limitType === "concurrent_sessions" ? 429 : 402;
  30. }
  31. export class ProxyErrorHandler {
  32. static async handle(session: ProxySession, error: unknown): Promise<Response> {
  33. // 分离两种消息:
  34. // - clientErrorMessage: 返回给客户端的安全消息(不含供应商名称)
  35. // - logErrorMessage: 记录到数据库的详细消息(包含供应商名称,便于排查)
  36. let clientErrorMessage: string;
  37. let logErrorMessage: string;
  38. let statusCode = 500;
  39. let rateLimitMetadata: Record<string, unknown> | null = null;
  40. // 优先处理 RateLimitError(新增)
  41. if (isRateLimitError(error)) {
  42. clientErrorMessage = error.message;
  43. logErrorMessage = error.message;
  44. // 使用 helper 函数计算状态码
  45. statusCode = getRateLimitStatusCode(error.limitType);
  46. rateLimitMetadata = error.toJSON();
  47. // 构建详细的 402 响应
  48. const response = ProxyErrorHandler.buildRateLimitResponse(error);
  49. // 记录错误到数据库(包含 rate_limit 元数据)
  50. await ProxyErrorHandler.logErrorToDatabase(
  51. session,
  52. logErrorMessage,
  53. statusCode,
  54. rateLimitMetadata
  55. );
  56. return response;
  57. }
  58. // 识别 ProxyError,提取详细信息(包含上游响应)
  59. if (error instanceof ProxyError) {
  60. // 客户端消息:不含供应商名称,保护敏感信息
  61. clientErrorMessage = error.getClientSafeMessage();
  62. // 日志消息:包含供应商名称,便于问题排查
  63. logErrorMessage = error.getDetailedErrorMessage();
  64. statusCode = error.statusCode; // 使用实际状态码(不再统一 5xx 为 500)
  65. } else if (isEmptyResponseError(error)) {
  66. // EmptyResponseError: 客户端消息不含供应商名称
  67. clientErrorMessage = error.getClientSafeMessage();
  68. logErrorMessage = error.message; // 日志保留完整信息
  69. statusCode = 502; // Bad Gateway
  70. } else if (error instanceof Error) {
  71. clientErrorMessage = error.message;
  72. logErrorMessage = error.message;
  73. } else {
  74. clientErrorMessage = "代理请求发生未知错误";
  75. logErrorMessage = "代理请求发生未知错误";
  76. }
  77. // 后备方案:如果状态码仍是 500,尝试从 provider chain 中提取最后一次实际请求的状态码
  78. if (statusCode === 500) {
  79. const lastRequestStatusCode = ProxyErrorHandler.getLastRequestStatusCode(session);
  80. if (lastRequestStatusCode && lastRequestStatusCode !== 200) {
  81. statusCode = lastRequestStatusCode;
  82. }
  83. }
  84. // 记录错误到数据库(始终记录详细错误消息,包含供应商名称)
  85. await ProxyErrorHandler.logErrorToDatabase(session, logErrorMessage, statusCode, null);
  86. // 检测是否有覆写配置(响应体或状态码)
  87. // 使用异步版本确保错误规则已加载
  88. if (error instanceof Error) {
  89. const override = await getErrorOverrideAsync(error);
  90. if (override) {
  91. // 运行时校验覆写状态码范围(400-599),防止数据库脏数据导致 Response 抛 RangeError
  92. let validatedStatusCode = override.statusCode;
  93. if (
  94. validatedStatusCode !== null &&
  95. (!Number.isInteger(validatedStatusCode) ||
  96. validatedStatusCode < OVERRIDE_STATUS_CODE_MIN ||
  97. validatedStatusCode > OVERRIDE_STATUS_CODE_MAX)
  98. ) {
  99. logger.warn("ProxyErrorHandler: Invalid override status code, falling back to upstream", {
  100. overrideStatusCode: validatedStatusCode,
  101. upstreamStatusCode: statusCode,
  102. });
  103. validatedStatusCode = null;
  104. }
  105. // 使用覆写状态码,如果未配置或无效则使用上游状态码
  106. const responseStatusCode = validatedStatusCode ?? statusCode;
  107. // 提取上游 request_id(用于覆写场景透传)
  108. const upstreamRequestId =
  109. error instanceof ProxyError ? error.upstreamError?.requestId : undefined;
  110. const safeRequestId = typeof upstreamRequestId === "string" ? upstreamRequestId : undefined;
  111. // 情况 1: 有响应体覆写 - 返回覆写的 JSON 响应
  112. if (override.response) {
  113. // 运行时守卫:验证覆写响应格式是否合法(双重保护,加载时已过滤一次)
  114. // 防止数据库中存在畸形数据导致返回不合规响应
  115. if (!isValidErrorOverrideResponse(override.response)) {
  116. logger.warn("ProxyErrorHandler: Invalid override response in database, skipping", {
  117. response: JSON.stringify(override.response).substring(0, 200),
  118. });
  119. // 跳过响应体覆写,但仍可应用状态码覆写
  120. if (override.statusCode !== null) {
  121. return ProxyResponses.buildError(
  122. responseStatusCode,
  123. clientErrorMessage,
  124. undefined,
  125. undefined,
  126. safeRequestId
  127. );
  128. }
  129. // 两者都无效,返回原始错误(但仍透传 request_id,因为有覆写意图)
  130. return ProxyResponses.buildError(
  131. statusCode,
  132. clientErrorMessage,
  133. undefined,
  134. undefined,
  135. safeRequestId
  136. );
  137. }
  138. // 覆写消息为空时回退到客户端安全消息
  139. const overrideErrorObj = override.response.error as Record<string, unknown>;
  140. const overrideMessage =
  141. typeof overrideErrorObj?.message === "string" &&
  142. overrideErrorObj.message.trim().length > 0
  143. ? overrideErrorObj.message
  144. : clientErrorMessage;
  145. // 构建覆写响应体
  146. // 设计原则:只输出用户配置的字段,不额外注入 request_id 等字段
  147. // 唯一的特殊处理:message 为空时回退到原始错误消息
  148. const responseBody = {
  149. ...override.response,
  150. error: {
  151. ...overrideErrorObj,
  152. message: overrideMessage,
  153. },
  154. };
  155. logger.info("ProxyErrorHandler: Applied error override response", {
  156. original: logErrorMessage.substring(0, 200),
  157. format: isClaudeErrorFormat(override.response)
  158. ? "claude"
  159. : isGeminiErrorFormat(override.response)
  160. ? "gemini"
  161. : isOpenAIErrorFormat(override.response)
  162. ? "openai"
  163. : "unknown",
  164. statusCode: responseStatusCode,
  165. });
  166. logger.error("ProxyErrorHandler: Request failed (overridden)", {
  167. error: logErrorMessage,
  168. statusCode: responseStatusCode,
  169. overridden: true,
  170. });
  171. return new Response(JSON.stringify(responseBody), {
  172. status: responseStatusCode,
  173. headers: { "Content-Type": "application/json" },
  174. });
  175. }
  176. // 情况 2: 仅状态码覆写 - 返回客户端安全消息,但使用覆写的状态码
  177. logger.info("ProxyErrorHandler: Applied status code override only", {
  178. original: logErrorMessage.substring(0, 200),
  179. originalStatusCode: statusCode,
  180. overrideStatusCode: responseStatusCode,
  181. hasRequestId: !!safeRequestId,
  182. });
  183. logger.error("ProxyErrorHandler: Request failed (status overridden)", {
  184. error: logErrorMessage,
  185. statusCode: responseStatusCode,
  186. overridden: true,
  187. });
  188. return ProxyResponses.buildError(
  189. responseStatusCode,
  190. clientErrorMessage,
  191. undefined,
  192. undefined,
  193. safeRequestId
  194. );
  195. }
  196. }
  197. logger.error("ProxyErrorHandler: Request failed", {
  198. error: logErrorMessage,
  199. statusCode,
  200. overridden: false,
  201. });
  202. return ProxyResponses.buildError(statusCode, clientErrorMessage);
  203. }
  204. /**
  205. * 构建 Rate Limit 响应(402/429)
  206. *
  207. * - RPM/并发用 429 Too Many Requests(可重试的频率控制)
  208. * - 消费限额用 402 Payment Required(需充值/等待重置)
  209. * 返回包含所有 7 个限流字段的详细错误信息,并添加标准 rate limit 响应头
  210. *
  211. * 响应体字段(7个核心字段):
  212. * - error.type: "rate_limit_error"
  213. * - error.message: 人类可读的错误消息
  214. * - error.code: 错误代码(固定为 "rate_limit_exceeded")
  215. * - error.limit_type: 限流类型(rpm/usd_5h/usd_weekly/usd_monthly/concurrent_sessions/daily_quota)
  216. * - error.current: 当前使用量
  217. * - error.limit: 限制值
  218. * - error.reset_time: 重置时间(ISO-8601格式)
  219. *
  220. * 响应头(3个标准 rate limit 头):
  221. * - X-RateLimit-Limit: 限制值
  222. * - X-RateLimit-Remaining: 剩余配额(max(0, limit - current))
  223. * - X-RateLimit-Reset: Unix 时间戳(秒)
  224. */
  225. private static buildRateLimitResponse(error: RateLimitError): Response {
  226. // 使用 helper 函数计算状态码
  227. const statusCode = getRateLimitStatusCode(error.limitType);
  228. // 计算剩余配额(不能为负数)
  229. const remaining = Math.max(0, error.limitValue - error.currentUsage);
  230. // 计算 Unix 时间戳(秒)
  231. const resetTimestamp = Math.floor(new Date(error.resetTime).getTime() / 1000);
  232. const headers = new Headers({
  233. "Content-Type": "application/json",
  234. // 标准 rate limit 响应头(3个)
  235. "X-RateLimit-Limit": error.limitValue.toString(),
  236. "X-RateLimit-Remaining": remaining.toString(),
  237. "X-RateLimit-Reset": resetTimestamp.toString(),
  238. // 额外的自定义头(便于客户端快速识别限流类型)
  239. "X-RateLimit-Type": error.limitType,
  240. "Retry-After": ProxyErrorHandler.calculateRetryAfter(error.resetTime),
  241. });
  242. return new Response(
  243. JSON.stringify({
  244. error: {
  245. // 保持向后兼容的核心字段
  246. type: error.type,
  247. message: error.message,
  248. // 新增字段(按任务要求的7个字段)
  249. code: "rate_limit_exceeded",
  250. limit_type: error.limitType,
  251. current: error.currentUsage,
  252. limit: error.limitValue,
  253. reset_time: error.resetTime,
  254. },
  255. }),
  256. {
  257. status: statusCode, // 根据 limitType 动态选择 429 或 402
  258. headers,
  259. }
  260. );
  261. }
  262. /**
  263. * 计算 Retry-After 头(秒数)
  264. */
  265. private static calculateRetryAfter(resetTime: string): string {
  266. const resetDate = new Date(resetTime);
  267. const now = new Date();
  268. const secondsUntilReset = Math.max(0, Math.ceil((resetDate.getTime() - now.getTime()) / 1000));
  269. return secondsUntilReset.toString();
  270. }
  271. /**
  272. * 记录错误到数据库
  273. *
  274. * 如果提供了 rateLimitMetadata,将其 JSON 序列化后存入 errorMessage
  275. * 供应商决策链保持不变,存入 providerChain 字段
  276. */
  277. private static async logErrorToDatabase(
  278. session: ProxySession,
  279. errorMessage: string,
  280. statusCode: number,
  281. rateLimitMetadata: Record<string, unknown> | null
  282. ): Promise<void> {
  283. if (!session.messageContext) {
  284. return;
  285. }
  286. const duration = Date.now() - session.startTime;
  287. await updateMessageRequestDuration(session.messageContext.id, duration);
  288. // 如果是限流错误,将元数据附加到错误消息中
  289. let finalErrorMessage = errorMessage;
  290. if (rateLimitMetadata) {
  291. finalErrorMessage = `${errorMessage} | rate_limit_metadata: ${JSON.stringify(rateLimitMetadata)}`;
  292. }
  293. // 保存错误信息和决策链
  294. await updateMessageRequestDetails(session.messageContext.id, {
  295. errorMessage: finalErrorMessage,
  296. providerChain: session.getProviderChain(),
  297. statusCode: statusCode,
  298. model: session.getCurrentModel() ?? undefined,
  299. providerId: session.provider?.id, // ⭐ 更新最终供应商ID(重试切换后)
  300. context1mApplied: session.getContext1mApplied(),
  301. });
  302. // 记录请求结束
  303. const tracker = ProxyStatusTracker.getInstance();
  304. tracker.endRequest(session.messageContext.user.id, session.messageContext.id);
  305. }
  306. /**
  307. * 从 provider chain 中提取最后一次实际请求的状态码
  308. */
  309. private static getLastRequestStatusCode(session: ProxySession): number | null {
  310. const chain = session.getProviderChain();
  311. if (!chain || chain.length === 0) {
  312. return null;
  313. }
  314. // 从后往前遍历,找到第一个有 statusCode 的记录(retry_failed 或 request_success)
  315. for (let i = chain.length - 1; i >= 0; i--) {
  316. const item = chain[i];
  317. if (item.statusCode && item.statusCode !== 200) {
  318. // 找到了失败的请求状态码
  319. return item.statusCode;
  320. }
  321. }
  322. return null;
  323. }
  324. }