transport-error-detection.test.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. /**
  2. * Transport Error Detection Tests
  3. *
  4. * Validates that isTransportError correctly classifies errors from:
  5. * - Agent pool destruction (UND_ERR_DESTROYED)
  6. * - HTTP/2 stream errors (ERR_HTTP2_STREAM_ERROR, NGHTTP2_INTERNAL_ERROR)
  7. * - Existing transport errors (ECONNRESET, etc.)
  8. */
  9. import { describe, expect, it } from "vitest";
  10. import { isTransportError, isHttp2Error } from "@/app/v1/_lib/proxy/errors";
  11. describe("isTransportError", () => {
  12. describe("existing transport errors (regression)", () => {
  13. it("should detect ECONNRESET", () => {
  14. const err = new Error("read ECONNRESET");
  15. (err as NodeJS.ErrnoException).code = "ECONNRESET";
  16. expect(isTransportError(err)).toBe(true);
  17. });
  18. it("should detect UND_ERR_SOCKET", () => {
  19. const err = new Error("Socket error");
  20. (err as NodeJS.ErrnoException).code = "UND_ERR_SOCKET";
  21. expect(isTransportError(err)).toBe(true);
  22. });
  23. it("should detect SocketError by name", () => {
  24. const err = new Error("Socket closed");
  25. err.name = "SocketError";
  26. expect(isTransportError(err)).toBe(true);
  27. });
  28. it("should detect 'other side closed' message", () => {
  29. const err = new Error("other side closed");
  30. expect(isTransportError(err)).toBe(true);
  31. });
  32. it("should detect 'fetch failed' message", () => {
  33. const err = new Error("fetch failed");
  34. expect(isTransportError(err)).toBe(true);
  35. });
  36. it("should not detect generic errors", () => {
  37. const err = new Error("Something went wrong");
  38. expect(isTransportError(err)).toBe(false);
  39. });
  40. });
  41. describe("agent destruction errors", () => {
  42. it("should detect UND_ERR_DESTROYED by code", () => {
  43. const err = new Error("The client is destroyed");
  44. (err as NodeJS.ErrnoException).code = "UND_ERR_DESTROYED";
  45. expect(isTransportError(err)).toBe(true);
  46. });
  47. it("should detect ClientDestroyedError by name", () => {
  48. const err = new Error("The client is destroyed");
  49. err.name = "ClientDestroyedError";
  50. expect(isTransportError(err)).toBe(true);
  51. });
  52. it("should detect UND_ERR_CLOSED by code", () => {
  53. const err = new Error("The client is closed");
  54. (err as NodeJS.ErrnoException).code = "UND_ERR_CLOSED";
  55. expect(isTransportError(err)).toBe(true);
  56. });
  57. it("should detect ClientClosedError by name", () => {
  58. const err = new Error("The client is closed");
  59. err.name = "ClientClosedError";
  60. expect(isTransportError(err)).toBe(true);
  61. });
  62. });
  63. describe("HTTP/2 stream errors", () => {
  64. it("should detect ERR_HTTP2_STREAM_ERROR via code", () => {
  65. const err = new Error("Stream closed with error code NGHTTP2_INTERNAL_ERROR");
  66. (err as NodeJS.ErrnoException).code = "ERR_HTTP2_STREAM_ERROR";
  67. expect(isTransportError(err)).toBe(true);
  68. });
  69. it("should detect NGHTTP2_INTERNAL_ERROR in message", () => {
  70. const err = new Error("Stream closed with error code NGHTTP2_INTERNAL_ERROR");
  71. expect(isTransportError(err)).toBe(true);
  72. });
  73. it("should detect GOAWAY errors", () => {
  74. const err = new Error("GOAWAY session");
  75. expect(isTransportError(err)).toBe(true);
  76. });
  77. it("should detect RST_STREAM errors", () => {
  78. const err = new Error("RST_STREAM received");
  79. expect(isTransportError(err)).toBe(true);
  80. });
  81. });
  82. describe("error code on cause", () => {
  83. it("should detect UND_ERR_DESTROYED on cause", () => {
  84. const cause = new Error("destroyed");
  85. (cause as NodeJS.ErrnoException).code = "UND_ERR_DESTROYED";
  86. const err = new Error("fetch failed");
  87. (err as Error & { cause: Error }).cause = cause;
  88. expect(isTransportError(err)).toBe(true);
  89. });
  90. it("should detect ERR_HTTP2_STREAM_ERROR on cause", () => {
  91. // ⭐ 回归:undici/fetch 会把底层 HTTP/2 错误包在 cause 里
  92. // 之前 isTransportError 只看顶层 code,漏检后会把真正的 transport 故障
  93. // 误判为供应商错误,直接把 agent 踢掉,反复触发 STREAM_PROCESSING_ERROR。
  94. // 注意:外层使用不匹配任何 message/name 签名的描述,确保走 cause.code 路径。
  95. const cause = new Error("Stream closed with error code");
  96. (cause as NodeJS.ErrnoException).code = "ERR_HTTP2_STREAM_ERROR";
  97. const err = new Error("request failed");
  98. (err as Error & { cause: Error }).cause = cause;
  99. expect(isTransportError(err)).toBe(true);
  100. });
  101. });
  102. });
  103. describe("isHttp2Error", () => {
  104. it("should detect ERR_HTTP2_GOAWAY_SESSION", () => {
  105. const err = new Error("ERR_HTTP2_GOAWAY_SESSION");
  106. expect(isHttp2Error(err)).toBe(true);
  107. });
  108. it("should detect NGHTTP2_INTERNAL_ERROR in message", () => {
  109. const err = new Error("Stream closed with error code NGHTTP2_INTERNAL_ERROR");
  110. expect(isHttp2Error(err)).toBe(true);
  111. });
  112. it("should detect ERR_HTTP2_STREAM_ERROR by code", () => {
  113. const err = new Error("Stream error");
  114. (err as NodeJS.ErrnoException).code = "ERR_HTTP2_STREAM_ERROR";
  115. expect(isHttp2Error(err)).toBe(true);
  116. });
  117. it("should not detect non-HTTP/2 errors", () => {
  118. const err = new Error("Connection refused");
  119. expect(isHttp2Error(err)).toBe(false);
  120. });
  121. });