retry.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { describe, expect, test } from "bun:test"
  2. import { SessionRetry } from "../../src/session/retry"
  3. import { MessageV2 } from "../../src/session/message-v2"
  4. function apiError(headers?: Record<string, string>): MessageV2.APIError {
  5. return new MessageV2.APIError({
  6. message: "boom",
  7. isRetryable: true,
  8. responseHeaders: headers,
  9. }).toObject() as MessageV2.APIError
  10. }
  11. describe("session.retry.delay", () => {
  12. test("caps delay at 30 seconds when headers missing", () => {
  13. const error = apiError()
  14. const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(index + 1, error))
  15. expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 30000, 30000, 30000, 30000, 30000, 30000])
  16. })
  17. test("prefers retry-after-ms when shorter than exponential", () => {
  18. const error = apiError({ "retry-after-ms": "1500" })
  19. expect(SessionRetry.delay(4, error)).toBe(1500)
  20. })
  21. test("uses retry-after seconds when reasonable", () => {
  22. const error = apiError({ "retry-after": "30" })
  23. expect(SessionRetry.delay(3, error)).toBe(30000)
  24. })
  25. test("accepts http-date retry-after values", () => {
  26. const date = new Date(Date.now() + 20000).toUTCString()
  27. const error = apiError({ "retry-after": date })
  28. const d = SessionRetry.delay(1, error)
  29. expect(d).toBeGreaterThanOrEqual(19000)
  30. expect(d).toBeLessThanOrEqual(20000)
  31. })
  32. test("ignores invalid retry hints", () => {
  33. const error = apiError({ "retry-after": "not-a-number" })
  34. expect(SessionRetry.delay(1, error)).toBe(2000)
  35. })
  36. test("ignores malformed date retry hints", () => {
  37. const error = apiError({ "retry-after": "Invalid Date String" })
  38. expect(SessionRetry.delay(1, error)).toBe(2000)
  39. })
  40. test("ignores past date retry hints", () => {
  41. const pastDate = new Date(Date.now() - 5000).toUTCString()
  42. const error = apiError({ "retry-after": pastDate })
  43. expect(SessionRetry.delay(1, error)).toBe(2000)
  44. })
  45. test("uses retry-after values even when exceeding 10 minutes with headers", () => {
  46. const error = apiError({ "retry-after": "50" })
  47. expect(SessionRetry.delay(1, error)).toBe(50000)
  48. const longError = apiError({ "retry-after-ms": "700000" })
  49. expect(SessionRetry.delay(1, longError)).toBe(700000)
  50. })
  51. })
  52. describe("session.message-v2.fromError", () => {
  53. test.concurrent(
  54. "converts ECONNRESET socket errors to retryable APIError",
  55. async () => {
  56. using server = Bun.serve({
  57. port: 0,
  58. idleTimeout: 8,
  59. async fetch(req) {
  60. return new Response(
  61. new ReadableStream({
  62. async pull(controller) {
  63. controller.enqueue("Hello,")
  64. await Bun.sleep(10000)
  65. controller.enqueue(" World!")
  66. controller.close()
  67. },
  68. }),
  69. { headers: { "Content-Type": "text/plain" } },
  70. )
  71. },
  72. })
  73. const error = await fetch(new URL("/", server.url.origin))
  74. .then((res) => res.text())
  75. .catch((e) => e)
  76. const result = MessageV2.fromError(error, { providerID: "test" })
  77. expect(MessageV2.APIError.isInstance(result)).toBe(true)
  78. expect((result as MessageV2.APIError).data.isRetryable).toBe(true)
  79. expect((result as MessageV2.APIError).data.message).toBe("Connection reset by server")
  80. expect((result as MessageV2.APIError).data.metadata?.code).toBe("ECONNRESET")
  81. expect((result as MessageV2.APIError).data.metadata?.message).toInclude("socket connection")
  82. },
  83. 15_000,
  84. )
  85. test("ECONNRESET socket error is retryable", () => {
  86. const error = new MessageV2.APIError({
  87. message: "Connection reset by server",
  88. isRetryable: true,
  89. metadata: { code: "ECONNRESET", message: "The socket connection was closed unexpectedly" },
  90. }).toObject() as MessageV2.APIError
  91. const retryable = SessionRetry.retryable(error)
  92. expect(retryable).toBeDefined()
  93. expect(retryable).toBe("Connection reset by server")
  94. })
  95. })