error-handler-verbose-provider-error-details.test.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. const mocks = vi.hoisted(() => {
  3. return {
  4. getCachedSystemSettings: vi.fn(async () => ({ verboseProviderError: false }) as any),
  5. getErrorOverrideAsync: vi.fn(async () => undefined),
  6. };
  7. });
  8. vi.mock("@/lib/config/system-settings-cache", () => ({
  9. getCachedSystemSettings: mocks.getCachedSystemSettings,
  10. }));
  11. vi.mock("@/lib/logger", () => ({
  12. logger: {
  13. debug: vi.fn(),
  14. info: vi.fn(),
  15. warn: vi.fn(),
  16. trace: vi.fn(),
  17. error: vi.fn(),
  18. fatal: vi.fn(),
  19. },
  20. }));
  21. vi.mock("@/app/v1/_lib/proxy/errors", async (importOriginal) => {
  22. const actual = await importOriginal<typeof import("@/app/v1/_lib/proxy/errors")>();
  23. return {
  24. ...actual,
  25. getErrorOverrideAsync: mocks.getErrorOverrideAsync,
  26. };
  27. });
  28. import { ProxyErrorHandler } from "@/app/v1/_lib/proxy/error-handler";
  29. import { EmptyResponseError, ProxyError } from "@/app/v1/_lib/proxy/errors";
  30. function createSession(): any {
  31. return {
  32. sessionId: null,
  33. messageContext: null,
  34. startTime: Date.now(),
  35. getProviderChain: () => [],
  36. getCurrentModel: () => null,
  37. getContext1mApplied: () => false,
  38. getGroupCostMultiplier: () => 1,
  39. provider: null,
  40. };
  41. }
  42. describe("ProxyErrorHandler.handle - verboseProviderError details", () => {
  43. beforeEach(() => {
  44. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: false } as any);
  45. mocks.getErrorOverrideAsync.mockResolvedValue(undefined);
  46. });
  47. test("verboseProviderError=false 时,不应附带 fake-200 raw body/details", async () => {
  48. const session = createSession();
  49. const err = new ProxyError("FAKE_200_JSON_ERROR_NON_EMPTY", 429, {
  50. body: "sanitized",
  51. providerId: 1,
  52. providerName: "p1",
  53. requestId: "req_123",
  54. rawBody: '{"error":"boom"}',
  55. rawBodyTruncated: false,
  56. statusCodeInferred: true,
  57. statusCodeInferenceMatcherId: "rate_limit",
  58. });
  59. const res = await ProxyErrorHandler.handle(session, err);
  60. expect(res.status).toBe(429);
  61. const body = await res.json();
  62. expect(body.error.details).toBeUndefined();
  63. expect(body.request_id).toBeUndefined();
  64. });
  65. test("verboseProviderError=true 时,fake-200 应返回详细报告与上游原文", async () => {
  66. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  67. const session = createSession();
  68. const err = new ProxyError("FAKE_200_HTML_BODY", 429, {
  69. body: "redacted snippet",
  70. providerId: 1,
  71. providerName: "p1",
  72. requestId: "req_123",
  73. rawBody: "<!doctype html><html><body>blocked</body></html>",
  74. rawBodyTruncated: false,
  75. statusCodeInferred: true,
  76. statusCodeInferenceMatcherId: "rate_limit",
  77. });
  78. const res = await ProxyErrorHandler.handle(session, err);
  79. expect(res.status).toBe(429);
  80. const body = await res.json();
  81. expect(body.request_id).toBe("req_123");
  82. expect(body.error.details).toEqual({
  83. upstreamError: {
  84. kind: "fake_200",
  85. code: "FAKE_200_HTML_BODY",
  86. statusCode: 429,
  87. statusCodeInferred: true,
  88. statusCodeInferenceMatcherId: "rate_limit",
  89. clientSafeMessage: expect.any(String),
  90. rawBody: "<!doctype html><html><body>blocked</body></html>",
  91. rawBodyTruncated: false,
  92. },
  93. });
  94. });
  95. test("verboseProviderError=true 时,rawBody 应做基础脱敏(避免泄露 token/key)", async () => {
  96. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  97. const session = createSession();
  98. const err = new ProxyError("FAKE_200_HTML_BODY", 429, {
  99. body: "redacted snippet",
  100. providerId: 1,
  101. providerName: "p1",
  102. requestId: "req_123",
  103. rawBody:
  104. "<!doctype html><html><body>Authorization: Bearer abc123 sk-1234567890abcdef1234567890 [email protected]</body></html>",
  105. rawBodyTruncated: false,
  106. statusCodeInferred: true,
  107. statusCodeInferenceMatcherId: "rate_limit",
  108. });
  109. const res = await ProxyErrorHandler.handle(session, err);
  110. expect(res.status).toBe(429);
  111. const body = await res.json();
  112. expect(body.request_id).toBe("req_123");
  113. expect(body.error.details.upstreamError.kind).toBe("fake_200");
  114. const rawBody = body.error.details.upstreamError.rawBody as string;
  115. expect(rawBody).toContain("Bearer [REDACTED]");
  116. expect(rawBody).toContain("[REDACTED_KEY]");
  117. expect(rawBody).toContain("[EMAIL]");
  118. expect(rawBody).not.toContain("Bearer abc123");
  119. expect(rawBody).not.toContain("sk-1234567890abcdef1234567890");
  120. expect(rawBody).not.toContain("[email protected]");
  121. });
  122. test("verboseProviderError=true 时,空响应错误也应返回详细报告(rawBody 为空字符串)", async () => {
  123. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  124. const session = createSession();
  125. const err = new EmptyResponseError(1, "p1", "empty_body");
  126. const res = await ProxyErrorHandler.handle(session, err);
  127. expect(res.status).toBe(502);
  128. const body = await res.json();
  129. expect(body.error.details).toEqual({
  130. upstreamError: {
  131. kind: "empty_response",
  132. reason: "empty_body",
  133. clientSafeMessage: "Empty response: Response body is empty",
  134. rawBody: "",
  135. rawBodyTruncated: false,
  136. },
  137. });
  138. });
  139. test("有 error override 时,verbose details 不应覆盖覆写逻辑(优先级更低)", async () => {
  140. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  141. mocks.getErrorOverrideAsync.mockResolvedValue({ response: null, statusCode: 418 });
  142. const session = createSession();
  143. const err = new ProxyError("FAKE_200_JSON_ERROR_NON_EMPTY", 429, {
  144. body: "sanitized",
  145. providerId: 1,
  146. providerName: "p1",
  147. requestId: "req_123",
  148. rawBody: '{"error":"boom"}',
  149. rawBodyTruncated: false,
  150. statusCodeInferred: true,
  151. statusCodeInferenceMatcherId: "rate_limit",
  152. });
  153. const res = await ProxyErrorHandler.handle(session, err);
  154. expect(res.status).toBe(418);
  155. const body = await res.json();
  156. expect(body.error.details).toBeUndefined();
  157. });
  158. test("有 response-body override 时,应返回覆写 body/status,且不混入 verbose details", async () => {
  159. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  160. mocks.getErrorOverrideAsync.mockResolvedValue({
  161. statusCode: 451,
  162. response: {
  163. error: {
  164. type: "invalid_request_error",
  165. message: "custom rewritten message",
  166. param: "messages",
  167. code: "provider_unavailable",
  168. },
  169. },
  170. });
  171. const session = createSession();
  172. const err = new ProxyError("FAKE_200_JSON_ERROR_NON_EMPTY", 429, {
  173. body: "sanitized",
  174. providerId: 1,
  175. providerName: "p1",
  176. requestId: "req_123",
  177. rawBody: '{"error":"boom"}',
  178. rawBodyTruncated: false,
  179. statusCodeInferred: true,
  180. statusCodeInferenceMatcherId: "rate_limit",
  181. });
  182. const res = await ProxyErrorHandler.handle(session, err);
  183. expect(res.status).toBe(451);
  184. const body = await res.json();
  185. expect(body).toEqual({
  186. error: {
  187. type: "invalid_request_error",
  188. message: "custom rewritten message",
  189. param: "messages",
  190. code: "provider_unavailable",
  191. },
  192. });
  193. expect(body.error.details).toBeUndefined();
  194. expect(body.request_id).toBeUndefined();
  195. });
  196. });