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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  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. provider: null,
  39. };
  40. }
  41. describe("ProxyErrorHandler.handle - verboseProviderError details", () => {
  42. beforeEach(() => {
  43. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: false } as any);
  44. mocks.getErrorOverrideAsync.mockResolvedValue(undefined);
  45. });
  46. test("verboseProviderError=false 时,不应附带 fake-200 raw body/details", async () => {
  47. const session = createSession();
  48. const err = new ProxyError("FAKE_200_JSON_ERROR_NON_EMPTY", 429, {
  49. body: "sanitized",
  50. providerId: 1,
  51. providerName: "p1",
  52. requestId: "req_123",
  53. rawBody: '{"error":"boom"}',
  54. rawBodyTruncated: false,
  55. statusCodeInferred: true,
  56. statusCodeInferenceMatcherId: "rate_limit",
  57. });
  58. const res = await ProxyErrorHandler.handle(session, err);
  59. expect(res.status).toBe(429);
  60. const body = await res.json();
  61. expect(body.error.details).toBeUndefined();
  62. expect(body.request_id).toBeUndefined();
  63. });
  64. test("verboseProviderError=true 时,fake-200 应返回详细报告与上游原文", async () => {
  65. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  66. const session = createSession();
  67. const err = new ProxyError("FAKE_200_HTML_BODY", 429, {
  68. body: "redacted snippet",
  69. providerId: 1,
  70. providerName: "p1",
  71. requestId: "req_123",
  72. rawBody: "<!doctype html><html><body>blocked</body></html>",
  73. rawBodyTruncated: false,
  74. statusCodeInferred: true,
  75. statusCodeInferenceMatcherId: "rate_limit",
  76. });
  77. const res = await ProxyErrorHandler.handle(session, err);
  78. expect(res.status).toBe(429);
  79. const body = await res.json();
  80. expect(body.request_id).toBe("req_123");
  81. expect(body.error.details).toEqual({
  82. upstreamError: {
  83. kind: "fake_200",
  84. code: "FAKE_200_HTML_BODY",
  85. statusCode: 429,
  86. statusCodeInferred: true,
  87. statusCodeInferenceMatcherId: "rate_limit",
  88. clientSafeMessage: expect.any(String),
  89. rawBody: "<!doctype html><html><body>blocked</body></html>",
  90. rawBodyTruncated: false,
  91. },
  92. });
  93. });
  94. test("verboseProviderError=true 时,rawBody 应做基础脱敏(避免泄露 token/key)", async () => {
  95. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  96. const session = createSession();
  97. const err = new ProxyError("FAKE_200_HTML_BODY", 429, {
  98. body: "redacted snippet",
  99. providerId: 1,
  100. providerName: "p1",
  101. requestId: "req_123",
  102. rawBody:
  103. "<!doctype html><html><body>Authorization: Bearer abc123 sk-1234567890abcdef1234567890 [email protected]</body></html>",
  104. rawBodyTruncated: false,
  105. statusCodeInferred: true,
  106. statusCodeInferenceMatcherId: "rate_limit",
  107. });
  108. const res = await ProxyErrorHandler.handle(session, err);
  109. expect(res.status).toBe(429);
  110. const body = await res.json();
  111. expect(body.request_id).toBe("req_123");
  112. expect(body.error.details.upstreamError.kind).toBe("fake_200");
  113. const rawBody = body.error.details.upstreamError.rawBody as string;
  114. expect(rawBody).toContain("Bearer [REDACTED]");
  115. expect(rawBody).toContain("[REDACTED_KEY]");
  116. expect(rawBody).toContain("[EMAIL]");
  117. expect(rawBody).not.toContain("Bearer abc123");
  118. expect(rawBody).not.toContain("sk-1234567890abcdef1234567890");
  119. expect(rawBody).not.toContain("[email protected]");
  120. });
  121. test("verboseProviderError=true 时,空响应错误也应返回详细报告(rawBody 为空字符串)", async () => {
  122. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  123. const session = createSession();
  124. const err = new EmptyResponseError(1, "p1", "empty_body");
  125. const res = await ProxyErrorHandler.handle(session, err);
  126. expect(res.status).toBe(502);
  127. const body = await res.json();
  128. expect(body.error.details).toEqual({
  129. upstreamError: {
  130. kind: "empty_response",
  131. reason: "empty_body",
  132. clientSafeMessage: "Empty response: Response body is empty",
  133. rawBody: "",
  134. rawBodyTruncated: false,
  135. },
  136. });
  137. });
  138. test("有 error override 时,verbose details 不应覆盖覆写逻辑(优先级更低)", async () => {
  139. mocks.getCachedSystemSettings.mockResolvedValue({ verboseProviderError: true } as any);
  140. mocks.getErrorOverrideAsync.mockResolvedValue({ response: null, statusCode: 418 });
  141. const session = createSession();
  142. const err = new ProxyError("FAKE_200_JSON_ERROR_NON_EMPTY", 429, {
  143. body: "sanitized",
  144. providerId: 1,
  145. providerName: "p1",
  146. requestId: "req_123",
  147. rawBody: '{"error":"boom"}',
  148. rawBodyTruncated: false,
  149. statusCodeInferred: true,
  150. statusCodeInferenceMatcherId: "rate_limit",
  151. });
  152. const res = await ProxyErrorHandler.handle(session, err);
  153. expect(res.status).toBe(418);
  154. const body = await res.json();
  155. expect(body.error.details).toBeUndefined();
  156. });
  157. });