client-abort-vs-upstream-499.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /**
  2. * Client Abort vs Upstream 499 Detection Tests
  3. *
  4. * Validates that isClientAbortError() and categorizeErrorAsync() correctly
  5. * distinguish between:
  6. * - Local client disconnection (CCH synthesized 499) -> CLIENT_ABORT
  7. * - Upstream HTTP 499 response -> PROVIDER_ERROR (triggers fallback/circuit-breaker)
  8. */
  9. import { describe, expect, it } from "vitest";
  10. import {
  11. ErrorCategory,
  12. ProxyError,
  13. categorizeErrorAsync,
  14. isClientAbortError,
  15. } from "@/app/v1/_lib/proxy/errors";
  16. describe("isClientAbortError - 499 source awareness", () => {
  17. // Scenario 1: Local abort (isLocalAbort=true) -> CLIENT_ABORT
  18. it("should detect ProxyError(499) with isLocalAbort=true as client abort", () => {
  19. const error = new ProxyError("Request aborted by client", 499, undefined, true);
  20. expect(isClientAbortError(error)).toBe(true);
  21. });
  22. // Scenario 2: Upstream 499 (default isLocalAbort=false) -> NOT client abort
  23. it("should NOT detect ProxyError(499) without isLocalAbort as client abort", () => {
  24. const error = new ProxyError("Provider returned 499", 499);
  25. expect(isClientAbortError(error)).toBe(false);
  26. });
  27. // Scenario 3: Upstream 499 with upstreamError details -> NOT client abort
  28. it("should NOT detect ProxyError(499) from upstream response as client abort", () => {
  29. const error = new ProxyError("Provider returned 499: Client Closed Request", 499, {
  30. body: '{"error": "client closed"}',
  31. parsed: { error: "client closed" },
  32. providerId: 1,
  33. providerName: "test-provider",
  34. });
  35. expect(isClientAbortError(error)).toBe(false);
  36. });
  37. // Scenario 4: Native AbortError (.name check) -> CLIENT_ABORT
  38. it("should detect native AbortError by name", () => {
  39. const error = new Error("The operation was aborted");
  40. error.name = "AbortError";
  41. expect(isClientAbortError(error)).toBe(true);
  42. });
  43. // Scenario 5: Native ResponseAborted (.name check) -> CLIENT_ABORT
  44. it("should detect native ResponseAborted by name", () => {
  45. const error = new Error("Response was aborted");
  46. error.name = "ResponseAborted";
  47. expect(isClientAbortError(error)).toBe(true);
  48. });
  49. // Scenario 6: Standard abort message -> CLIENT_ABORT
  50. it('should detect "This operation was aborted" message', () => {
  51. const error = new Error("This operation was aborted");
  52. expect(isClientAbortError(error)).toBe(true);
  53. });
  54. // Scenario 7: Browser standard abort message -> CLIENT_ABORT
  55. it('should detect "The user aborted a request" message', () => {
  56. const error = new Error("The user aborted a request");
  57. expect(isClientAbortError(error)).toBe(true);
  58. });
  59. // Scenario 8: Server-side abort message should NOT match (removed broad "aborted" match)
  60. it('should NOT detect "Transaction aborted by server" as client abort', () => {
  61. const error = new Error("Transaction aborted by server");
  62. expect(isClientAbortError(error)).toBe(false);
  63. });
  64. // Scenario 9: Non-499 ProxyError with isLocalAbort=true should NOT match (only 499 matters)
  65. it("should NOT detect non-499 ProxyError as client abort even with isLocalAbort=true", () => {
  66. const error = new ProxyError("Bad Gateway", 502, undefined, true);
  67. expect(isClientAbortError(error)).toBe(false);
  68. });
  69. });
  70. describe("categorizeErrorAsync - 499 source awareness", () => {
  71. // Scenario 1: Local 499 -> CLIENT_ABORT
  72. it("should categorize local 499 (isLocalAbort=true) as CLIENT_ABORT", async () => {
  73. const error = new ProxyError("Request aborted by client", 499, undefined, true);
  74. expect(await categorizeErrorAsync(error)).toBe(ErrorCategory.CLIENT_ABORT);
  75. });
  76. // Scenario 2: Upstream 499 (default) -> PROVIDER_ERROR
  77. it("should categorize upstream 499 (isLocalAbort=false) as PROVIDER_ERROR", async () => {
  78. const error = new ProxyError("Provider returned 499", 499);
  79. expect(await categorizeErrorAsync(error)).toBe(ErrorCategory.PROVIDER_ERROR);
  80. });
  81. // Scenario 3: Upstream 499 with upstreamError -> PROVIDER_ERROR
  82. it("should categorize upstream 499 with error details as PROVIDER_ERROR", async () => {
  83. const error = new ProxyError("Provider returned 499: Client Closed Request", 499, {
  84. body: '{"error": "client closed"}',
  85. parsed: { error: "client closed" },
  86. providerId: 1,
  87. providerName: "test-provider",
  88. });
  89. expect(await categorizeErrorAsync(error)).toBe(ErrorCategory.PROVIDER_ERROR);
  90. });
  91. });
  92. describe("ProxyError.fromUpstreamResponse - isLocalAbort default", () => {
  93. // Scenario 9: fromUpstreamResponse should produce isLocalAbort=false
  94. it("should create ProxyError with isLocalAbort=false from upstream 499 response", async () => {
  95. const fakeResponse = new Response('{"error": "client closed"}', {
  96. status: 499,
  97. statusText: "Client Closed Request",
  98. headers: { "content-type": "application/json" },
  99. });
  100. const error = await ProxyError.fromUpstreamResponse(fakeResponse, {
  101. id: 1,
  102. name: "test-provider",
  103. });
  104. expect(error.statusCode).toBe(499);
  105. expect(error.isLocalAbort).toBe(false);
  106. expect(isClientAbortError(error)).toBe(false);
  107. });
  108. });
  109. describe("ProxyError.isLocalAbort property", () => {
  110. it("should default isLocalAbort to false when not specified", () => {
  111. const error = new ProxyError("test", 499);
  112. expect(error.isLocalAbort).toBe(false);
  113. });
  114. it("should set isLocalAbort to true when explicitly passed", () => {
  115. const error = new ProxyError("test", 499, undefined, true);
  116. expect(error.isLocalAbort).toBe(true);
  117. });
  118. it("should set isLocalAbort to false when explicitly passed", () => {
  119. const error = new ProxyError("test", 499, undefined, false);
  120. expect(error.isLocalAbort).toBe(false);
  121. });
  122. });