@internetarchive/fetch-handler 1.1.0-webdev-7731.4 → 1.1.0-webdev-7731.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,9 @@ export declare class DefaultRetryConfiguration implements RetryConfiguring {
5
5
  private readonly maxRetries;
6
6
  constructor(options?: {
7
7
  maxRetries?: number;
8
+ transientStatusCodes?: Set<number>;
8
9
  });
10
+ readonly transientStatusCodes: ReadonlySet<number>;
9
11
  shouldRetry(response: Response | null, retryNumber: number): boolean;
10
- retryDelay(retryNumber: number): Milliseconds;
12
+ retryDelay(retryNumber: number, response?: Response | null): Milliseconds;
11
13
  }
@@ -1,18 +1,39 @@
1
1
  export class DefaultRetryConfiguration {
2
2
  constructor(options) {
3
3
  this.maxRetries = 2;
4
+ this.transientStatusCodes = new Set([
5
+ 408, // Request Timeout
6
+ 429, // Too Many Requests
7
+ 500, // Internal Server Error
8
+ 502, // Bad Gateway
9
+ 503, // Service Unavailable
10
+ 504, // Gateway Timeout
11
+ 522, // Cloudflare Origin Server Connection Timed Out
12
+ ]);
4
13
  if ((options === null || options === void 0 ? void 0 : options.maxRetries) !== undefined) {
5
14
  this.maxRetries = options.maxRetries;
6
15
  }
16
+ if ((options === null || options === void 0 ? void 0 : options.transientStatusCodes) !== undefined) {
17
+ this.transientStatusCodes = options.transientStatusCodes;
18
+ }
7
19
  }
8
20
  shouldRetry(response, retryNumber) {
9
21
  if (response === null)
10
22
  return false;
11
23
  if (retryNumber > this.maxRetries)
12
24
  return false;
13
- return response.status >= 500 && response.status < 600;
25
+ const isTransient = this.transientStatusCodes.has(response.status);
26
+ return isTransient;
14
27
  }
15
- retryDelay(retryNumber) {
28
+ retryDelay(retryNumber, response) {
29
+ // If we have a Retry-After header, use that
30
+ const retryAfter = response === null || response === void 0 ? void 0 : response.headers.get('Retry-After');
31
+ if (retryAfter) {
32
+ const retryAfterSeconds = parseInt(retryAfter, 10);
33
+ if (!isNaN(retryAfterSeconds)) {
34
+ return retryAfterSeconds * 1000;
35
+ }
36
+ }
16
37
  // Exponential backoff up to 10 seconds
17
38
  return Math.min(500 * 2 ** retryNumber, 10000);
18
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"default-retry-configuration.js","sourceRoot":"","sources":["../../../../src/fetch-retry/configuration/default-retry-configuration.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,yBAAyB;IAMpC,YAAY,OAAiC;QAF5B,eAAU,GAAqB,CAAC,CAAC;QAGhD,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,MAAK,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACvC,CAAC;IACH,CAAC;IAED,WAAW,CAAC,QAAyB,EAAE,WAAmB;QACxD,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QACpC,IAAI,WAAW,GAAG,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QAChD,OAAO,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC;IACzD,CAAC;IAED,UAAU,CAAC,WAAmB;QAC5B,uCAAuC;QACvC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,WAAW,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;;AApBe,gCAAM,GACpB,IAAI,yBAAyB,EAAE,AADX,CACY","sourcesContent":["import type { RetryConfiguring } from './retry-configuring';\nimport type { Milliseconds } from './milliseconds';\n\nexport class DefaultRetryConfiguration implements RetryConfiguring {\n static readonly shared: Readonly<RetryConfiguring> =\n new DefaultRetryConfiguration();\n\n private readonly maxRetries: Readonly<number> = 2;\n\n constructor(options?: { maxRetries?: number }) {\n if (options?.maxRetries !== undefined) {\n this.maxRetries = options.maxRetries;\n }\n }\n\n shouldRetry(response: Response | null, retryNumber: number): boolean {\n if (response === null) return false;\n if (retryNumber > this.maxRetries) return false;\n return response.status >= 500 && response.status < 600;\n }\n\n retryDelay(retryNumber: number): Milliseconds {\n // Exponential backoff up to 10 seconds\n return Math.min(500 * 2 ** retryNumber, 10000);\n }\n}\n"]}
1
+ {"version":3,"file":"default-retry-configuration.js","sourceRoot":"","sources":["../../../../src/fetch-retry/configuration/default-retry-configuration.ts"],"names":[],"mappings":"AAGA,MAAM,OAAO,yBAAyB;IAMpC,YAAY,OAGX;QALgB,eAAU,GAAqB,CAAC,CAAC;QAczC,yBAAoB,GAAwB,IAAI,GAAG,CAAC;YAC3D,GAAG,EAAE,kBAAkB;YACvB,GAAG,EAAE,oBAAoB;YACzB,GAAG,EAAE,wBAAwB;YAC7B,GAAG,EAAE,cAAc;YACnB,GAAG,EAAE,sBAAsB;YAC3B,GAAG,EAAE,kBAAkB;YACvB,GAAG,EAAE,gDAAgD;SACtD,CAAC,CAAC;QAhBD,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,MAAK,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACvC,CAAC;QACD,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,oBAAoB,MAAK,SAAS,EAAE,CAAC;YAChD,IAAI,CAAC,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC;QAC3D,CAAC;IACH,CAAC;IAYD,WAAW,CAAC,QAAyB,EAAE,WAAmB;QACxD,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QACpC,IAAI,WAAW,GAAG,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QAChD,MAAM,WAAW,GAAG,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnE,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,UAAU,CAAC,WAAmB,EAAE,QAA0B;QACxD,4CAA4C;QAC5C,MAAM,UAAU,GAAG,QAAQ,aAAR,QAAQ,uBAAR,QAAQ,CAAE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxD,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,iBAAiB,GAAG,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YACnD,IAAI,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBAC9B,OAAO,iBAAiB,GAAG,IAAI,CAAC;YAClC,CAAC;QACH,CAAC;QAED,uCAAuC;QACvC,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,WAAW,EAAE,KAAK,CAAC,CAAC;IACjD,CAAC;;AA9Ce,gCAAM,GACpB,IAAI,yBAAyB,EAAE,AADX,CACY","sourcesContent":["import type { RetryConfiguring } from './retry-configuring';\nimport type { Milliseconds } from './milliseconds';\n\nexport class DefaultRetryConfiguration implements RetryConfiguring {\n static readonly shared: Readonly<RetryConfiguring> =\n new DefaultRetryConfiguration();\n\n private readonly maxRetries: Readonly<number> = 2;\n\n constructor(options?: {\n maxRetries?: number;\n transientStatusCodes?: Set<number>;\n }) {\n if (options?.maxRetries !== undefined) {\n this.maxRetries = options.maxRetries;\n }\n if (options?.transientStatusCodes !== undefined) {\n this.transientStatusCodes = options.transientStatusCodes;\n }\n }\n\n readonly transientStatusCodes: ReadonlySet<number> = new Set([\n 408, // Request Timeout\n 429, // Too Many Requests\n 500, // Internal Server Error\n 502, // Bad Gateway\n 503, // Service Unavailable\n 504, // Gateway Timeout\n 522, // Cloudflare Origin Server Connection Timed Out\n ]);\n\n shouldRetry(response: Response | null, retryNumber: number): boolean {\n if (response === null) return false;\n if (retryNumber > this.maxRetries) return false;\n const isTransient = this.transientStatusCodes.has(response.status);\n return isTransient;\n }\n\n retryDelay(retryNumber: number, response?: Response | null): Milliseconds {\n // If we have a Retry-After header, use that\n const retryAfter = response?.headers.get('Retry-After');\n if (retryAfter) {\n const retryAfterSeconds = parseInt(retryAfter, 10);\n if (!isNaN(retryAfterSeconds)) {\n return retryAfterSeconds * 1000;\n }\n }\n\n // Exponential backoff up to 10 seconds\n return Math.min(500 * 2 ** retryNumber, 10000);\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { Milliseconds } from './milliseconds';
2
2
  export interface RetryConfiguring {
3
3
  shouldRetry(response: Response | null, retryNumber: number, error?: unknown): boolean;
4
- retryDelay(retryNumber: number): Milliseconds;
4
+ retryDelay(retryNumber: number, response?: Response | null): Milliseconds;
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"file":"retry-configuring.js","sourceRoot":"","sources":["../../../../src/fetch-retry/configuration/retry-configuring.ts"],"names":[],"mappings":"","sourcesContent":["import type { Milliseconds } from './milliseconds';\n\nexport interface RetryConfiguring {\n shouldRetry(\n response: Response | null,\n retryNumber: number,\n error?: unknown,\n ): boolean;\n\n retryDelay(retryNumber: number): Milliseconds;\n}\n"]}
1
+ {"version":3,"file":"retry-configuring.js","sourceRoot":"","sources":["../../../../src/fetch-retry/configuration/retry-configuring.ts"],"names":[],"mappings":"","sourcesContent":["import type { Milliseconds } from './milliseconds';\n\nexport interface RetryConfiguring {\n shouldRetry(\n response: Response | null,\n retryNumber: number,\n error?: unknown,\n ): boolean;\n\n retryDelay(retryNumber: number, response?: Response | null): Milliseconds;\n}\n"]}
@@ -28,7 +28,7 @@ export class FetchRetrier {
28
28
  }
29
29
  const retryConfig = (_a = options === null || options === void 0 ? void 0 : options.retryConfig) !== null && _a !== void 0 ? _a : this.retryConfiguration;
30
30
  if (retryConfig.shouldRetry(response, retryNumber)) {
31
- const delay = retryConfig.retryDelay(retryNumber);
31
+ const delay = retryConfig.retryDelay(retryNumber, response);
32
32
  await promisedSleep(delay);
33
33
  this.logRetryEvent(urlString, retryNumber, response.statusText, response.status);
34
34
  return this.fetchRetryWithOptions(request, retryNumber + 1, options);
@@ -1 +1 @@
1
- {"version":3,"file":"fetch-retrier.js","sourceRoot":"","sources":["../../../src/fetch-retry/fetch-retrier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AnalyticsHandlerInterface } from '@internetarchive/analytics-manager';\nimport { promisedSleep } from '../utils/promised-sleep';\nimport { type FetchOptions } from '../fetch-options';\nimport { legacyArgsAsFetchOptions } from './legacy-args';\nimport { DefaultRetryConfiguration } from './configuration/default-retry-configuration';\nimport type { RetryConfiguring } from './configuration/retry-configuring';\n\n/**\n * A class that retries a fetch request.\n */\nexport interface FetchRetrierInterface {\n /**\n * Execute a fetch with retry.\n *\n * @param request RequestInfo\n * @param options Optional RequestInit | FetchOptions\n * @returns Promise<Response>\n */\n fetchRetry(\n request: RequestInfo,\n options?: RequestInit | FetchOptions,\n ): Promise<Response>;\n}\n\n/** @inheritdoc */\nexport class FetchRetrier implements FetchRetrierInterface {\n private analyticsHandler?: AnalyticsHandlerInterface;\n\n private retryConfiguration: RetryConfiguring =\n new DefaultRetryConfiguration();\n\n constructor(options?: {\n analyticsHandler?: AnalyticsHandlerInterface;\n retryConfiguration?: RetryConfiguring;\n }) {\n if (options?.analyticsHandler)\n this.analyticsHandler = options.analyticsHandler;\n if (options?.retryConfiguration)\n this.retryConfiguration = options.retryConfiguration;\n }\n\n /** @inheritdoc */\n public async fetchRetry(\n request: RequestInfo,\n options?: RequestInit | FetchOptions,\n ): Promise<Response> {\n const fetchOptions = legacyArgsAsFetchOptions(options);\n return await this.fetchRetryWithOptions(request, 0, fetchOptions);\n }\n\n private async fetchRetryWithOptions(\n request: RequestInfo,\n retryNumber: number,\n options?: FetchOptions,\n ): Promise<Response> {\n const urlString = typeof request === 'string' ? request : request.url;\n\n try {\n const response = await fetch(request, options?.requestInit);\n if (response.ok) return response;\n\n if (response.status >= 400 && response.status < 500) {\n this.log4xxResponse(response);\n }\n\n const retryConfig = options?.retryConfig ?? this.retryConfiguration;\n if (retryConfig.shouldRetry(response, retryNumber)) {\n const delay = retryConfig.retryDelay(retryNumber);\n await promisedSleep(delay);\n this.logRetryEvent(\n urlString,\n retryNumber,\n response.statusText,\n response.status,\n );\n return this.fetchRetryWithOptions(request, retryNumber + 1, options);\n }\n this.logFailureEvent(urlString, response.status);\n return response;\n } catch (error) {\n // if a content blocker is detected, log it and don't retry\n if (this.isContentBlockerError(error)) {\n this.logContentBlockingEvent(urlString, error);\n throw error;\n }\n\n const retryConfig = options?.retryConfig ?? this.retryConfiguration;\n if (retryConfig.shouldRetry(null, retryNumber)) {\n const delay = retryConfig.retryDelay(retryNumber);\n await promisedSleep(delay);\n this.logRetryEvent(urlString, retryNumber, error, error);\n return this.fetchRetryWithOptions(request, retryNumber + 1, options);\n }\n this.logFailureEvent(urlString, error);\n throw error;\n }\n }\n\n private isContentBlockerError(error: unknown): boolean {\n // all of the content blocker errors are `TypeError`\n if (!(error instanceof TypeError)) return false;\n const message = error.message.toLowerCase();\n return message.includes('content blocker');\n }\n\n private readonly eventCategory = 'offshootFetchRetry';\n\n private logRetryEvent(\n urlString: string,\n retryNumber: number,\n status: unknown,\n code: unknown,\n ) {\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: 'retryingFetch',\n label: `retryNumber: ${retryNumber}, code: ${code}, status: ${status}, url: ${urlString}`,\n });\n }\n\n private logFailureEvent(urlString: string, error: unknown) {\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: 'fetchFailed',\n label: `error: ${error}, url: ${urlString}`,\n });\n }\n\n private log4xxResponse(response: Response) {\n const status = response.status;\n\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: `status4xxResponse`,\n label: `http status ${status}, url: ${response.url}`,\n });\n }\n\n private logContentBlockingEvent(urlString: string, error: unknown) {\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: 'contentBlockerDetectedNotRetrying',\n label: `error: ${error}, url: ${urlString}`,\n });\n }\n}\n"]}
1
+ {"version":3,"file":"fetch-retrier.js","sourceRoot":"","sources":["../../../src/fetch-retry/fetch-retrier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AnalyticsHandlerInterface } from '@internetarchive/analytics-manager';\nimport { promisedSleep } from '../utils/promised-sleep';\nimport { type FetchOptions } from '../fetch-options';\nimport { legacyArgsAsFetchOptions } from './legacy-args';\nimport { DefaultRetryConfiguration } from './configuration/default-retry-configuration';\nimport type { RetryConfiguring } from './configuration/retry-configuring';\n\n/**\n * A class that retries a fetch request.\n */\nexport interface FetchRetrierInterface {\n /**\n * Execute a fetch with retry.\n *\n * @param request RequestInfo\n * @param options Optional RequestInit | FetchOptions\n * @returns Promise<Response>\n */\n fetchRetry(\n request: RequestInfo,\n options?: RequestInit | FetchOptions,\n ): Promise<Response>;\n}\n\n/** @inheritdoc */\nexport class FetchRetrier implements FetchRetrierInterface {\n private analyticsHandler?: AnalyticsHandlerInterface;\n\n private retryConfiguration: RetryConfiguring =\n new DefaultRetryConfiguration();\n\n constructor(options?: {\n analyticsHandler?: AnalyticsHandlerInterface;\n retryConfiguration?: RetryConfiguring;\n }) {\n if (options?.analyticsHandler)\n this.analyticsHandler = options.analyticsHandler;\n if (options?.retryConfiguration)\n this.retryConfiguration = options.retryConfiguration;\n }\n\n /** @inheritdoc */\n public async fetchRetry(\n request: RequestInfo,\n options?: RequestInit | FetchOptions,\n ): Promise<Response> {\n const fetchOptions = legacyArgsAsFetchOptions(options);\n return await this.fetchRetryWithOptions(request, 0, fetchOptions);\n }\n\n private async fetchRetryWithOptions(\n request: RequestInfo,\n retryNumber: number,\n options?: FetchOptions,\n ): Promise<Response> {\n const urlString = typeof request === 'string' ? request : request.url;\n\n try {\n const response = await fetch(request, options?.requestInit);\n if (response.ok) return response;\n\n if (response.status >= 400 && response.status < 500) {\n this.log4xxResponse(response);\n }\n\n const retryConfig = options?.retryConfig ?? this.retryConfiguration;\n if (retryConfig.shouldRetry(response, retryNumber)) {\n const delay = retryConfig.retryDelay(retryNumber, response);\n await promisedSleep(delay);\n this.logRetryEvent(\n urlString,\n retryNumber,\n response.statusText,\n response.status,\n );\n return this.fetchRetryWithOptions(request, retryNumber + 1, options);\n }\n this.logFailureEvent(urlString, response.status);\n return response;\n } catch (error) {\n // if a content blocker is detected, log it and don't retry\n if (this.isContentBlockerError(error)) {\n this.logContentBlockingEvent(urlString, error);\n throw error;\n }\n\n const retryConfig = options?.retryConfig ?? this.retryConfiguration;\n if (retryConfig.shouldRetry(null, retryNumber)) {\n const delay = retryConfig.retryDelay(retryNumber);\n await promisedSleep(delay);\n this.logRetryEvent(urlString, retryNumber, error, error);\n return this.fetchRetryWithOptions(request, retryNumber + 1, options);\n }\n this.logFailureEvent(urlString, error);\n throw error;\n }\n }\n\n private isContentBlockerError(error: unknown): boolean {\n // all of the content blocker errors are `TypeError`\n if (!(error instanceof TypeError)) return false;\n const message = error.message.toLowerCase();\n return message.includes('content blocker');\n }\n\n private readonly eventCategory = 'offshootFetchRetry';\n\n private logRetryEvent(\n urlString: string,\n retryNumber: number,\n status: unknown,\n code: unknown,\n ) {\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: 'retryingFetch',\n label: `retryNumber: ${retryNumber}, code: ${code}, status: ${status}, url: ${urlString}`,\n });\n }\n\n private logFailureEvent(urlString: string, error: unknown) {\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: 'fetchFailed',\n label: `error: ${error}, url: ${urlString}`,\n });\n }\n\n private log4xxResponse(response: Response) {\n const status = response.status;\n\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: `status4xxResponse`,\n label: `http status ${status}, url: ${response.url}`,\n });\n }\n\n private logContentBlockingEvent(urlString: string, error: unknown) {\n this.analyticsHandler?.sendEvent({\n category: this.eventCategory,\n action: 'contentBlockerDetectedNotRetrying',\n label: `error: ${error}, url: ${urlString}`,\n });\n }\n}\n"]}
@@ -10,21 +10,38 @@ describe('DefaultRetryConfiguration', () => {
10
10
  const mockResponse = new Response(null, { status: 500 });
11
11
  expect(config.shouldRetry(mockResponse, 3)).to.be.false;
12
12
  });
13
- it('should retry on 5xx status codes', async () => {
14
- const config = new DefaultRetryConfiguration();
15
- const mockResponse = new Response(null, { status: 502 });
16
- expect(config.shouldRetry(mockResponse, 1)).to.be.true;
13
+ it('should retry transient status codes', async () => {
14
+ const transientStatuses = [408, 429, 500, 502, 503, 504];
15
+ const config = new DefaultRetryConfiguration({
16
+ transientStatusCodes: new Set(transientStatuses),
17
+ });
18
+ for (const status of transientStatuses) {
19
+ const mockResponse = new Response(null, { status });
20
+ expect(config.shouldRetry(mockResponse, 1)).to.be.true;
21
+ }
17
22
  });
18
- it('should not retry on non-5xx status codes', async () => {
23
+ it('should not retry non-transient status codes', async () => {
19
24
  const config = new DefaultRetryConfiguration();
20
25
  const mockResponse = new Response(null, { status: 404 });
21
26
  expect(config.shouldRetry(mockResponse, 1)).to.be.false;
22
27
  });
28
+ it('uses Retry-After header if present', async () => {
29
+ const config = new DefaultRetryConfiguration();
30
+ const headers = new Headers();
31
+ headers.append('Retry-After', '3');
32
+ const mockResponse = new Response(null, { status: 503, headers });
33
+ expect(config.retryDelay(0, mockResponse)).to.equal(3000);
34
+ });
23
35
  it('has exponential backoff delay', async () => {
24
36
  const config = new DefaultRetryConfiguration();
25
37
  expect(config.retryDelay(0)).to.equal(500);
26
38
  expect(config.retryDelay(1)).to.equal(1000);
27
39
  expect(config.retryDelay(2)).to.equal(2000);
28
40
  });
41
+ it('caps retry delay at 10 seconds', async () => {
42
+ const config = new DefaultRetryConfiguration();
43
+ expect(config.retryDelay(10)).to.equal(10000);
44
+ expect(config.retryDelay(20)).to.equal(10000);
45
+ });
29
46
  });
30
47
  //# sourceMappingURL=default-retry-config.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"default-retry-config.test.js","sourceRoot":"","sources":["../../test/default-retry-config.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,yBAAyB,EAAE,MAAM,8DAA8D,CAAC;AAEzG,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,mCAAmC,EAAE,KAAK,IAAI,EAAE;QACjD,MAAM,MAAM,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,MAAM,GAAG,IAAI,yBAAyB,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QAChE,MAAM,YAAY,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,MAAM,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAC/C,MAAM,YAAY,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,MAAM,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAC/C,MAAM,YAAY,GAAG,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC7C,MAAM,MAAM,GAAG,IAAI,yBAAyB,EAAE,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { DefaultRetryConfiguration } from '../src/fetch-retry/configuration/default-retry-configuration';\n\ndescribe('DefaultRetryConfiguration', () => {\n it('should not retry on null response', async () => {\n const config = new DefaultRetryConfiguration();\n expect(config.shouldRetry(null, 1)).to.be.false;\n });\n\n it('should not retry after max retries exceeded', async () => {\n const config = new DefaultRetryConfiguration({ maxRetries: 2 });\n const mockResponse = new Response(null, { status: 500 });\n expect(config.shouldRetry(mockResponse, 3)).to.be.false;\n });\n\n it('should retry on 5xx status codes', async () => {\n const config = new DefaultRetryConfiguration();\n const mockResponse = new Response(null, { status: 502 });\n expect(config.shouldRetry(mockResponse, 1)).to.be.true;\n });\n\n it('should not retry on non-5xx status codes', async () => {\n const config = new DefaultRetryConfiguration();\n const mockResponse = new Response(null, { status: 404 });\n expect(config.shouldRetry(mockResponse, 1)).to.be.false;\n });\n\n it('has exponential backoff delay', async () => {\n const config = new DefaultRetryConfiguration();\n expect(config.retryDelay(0)).to.equal(500);\n expect(config.retryDelay(1)).to.equal(1000);\n expect(config.retryDelay(2)).to.equal(2000);\n });\n});\n"]}
1
+ {"version":3,"file":"default-retry-config.test.js","sourceRoot":"","sources":["../../test/default-retry-config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { DefaultRetryConfiguration } from '../src/fetch-retry/configuration/default-retry-configuration';\n\ndescribe('DefaultRetryConfiguration', () => {\n it('should not retry on null response', async () => {\n const config = new DefaultRetryConfiguration();\n expect(config.shouldRetry(null, 1)).to.be.false;\n });\n\n it('should not retry after max retries exceeded', async () => {\n const config = new DefaultRetryConfiguration({ maxRetries: 2 });\n const mockResponse = new Response(null, { status: 500 });\n expect(config.shouldRetry(mockResponse, 3)).to.be.false;\n });\n\n it('should retry transient status codes', async () => {\n const transientStatuses = [408, 429, 500, 502, 503, 504];\n const config = new DefaultRetryConfiguration({\n transientStatusCodes: new Set(transientStatuses),\n });\n for (const status of transientStatuses) {\n const mockResponse = new Response(null, { status });\n expect(config.shouldRetry(mockResponse, 1)).to.be.true;\n }\n });\n\n it('should not retry non-transient status codes', async () => {\n const config = new DefaultRetryConfiguration();\n const mockResponse = new Response(null, { status: 404 });\n expect(config.shouldRetry(mockResponse, 1)).to.be.false;\n });\n\n it('uses Retry-After header if present', async () => {\n const config = new DefaultRetryConfiguration();\n const headers = new Headers();\n headers.append('Retry-After', '3');\n const mockResponse = new Response(null, { status: 503, headers });\n expect(config.retryDelay(0, mockResponse)).to.equal(3000);\n });\n\n it('has exponential backoff delay', async () => {\n const config = new DefaultRetryConfiguration();\n expect(config.retryDelay(0)).to.equal(500);\n expect(config.retryDelay(1)).to.equal(1000);\n expect(config.retryDelay(2)).to.equal(2000);\n });\n\n it('caps retry delay at 10 seconds', async () => {\n const config = new DefaultRetryConfiguration();\n expect(config.retryDelay(10)).to.equal(10000);\n expect(config.retryDelay(20)).to.equal(10000);\n });\n});\n"]}
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "AGPL-3.0-only",
9
9
  "author": "Internet Archive",
10
- "version": "1.1.0-webdev-7731.4",
10
+ "version": "1.1.0-webdev-7731.6",
11
11
  "main": "dist/index.js",
12
12
  "module": "dist/index.js",
13
13
  "scripts": {
@@ -7,19 +7,45 @@ export class DefaultRetryConfiguration implements RetryConfiguring {
7
7
 
8
8
  private readonly maxRetries: Readonly<number> = 2;
9
9
 
10
- constructor(options?: { maxRetries?: number }) {
10
+ constructor(options?: {
11
+ maxRetries?: number;
12
+ transientStatusCodes?: Set<number>;
13
+ }) {
11
14
  if (options?.maxRetries !== undefined) {
12
15
  this.maxRetries = options.maxRetries;
13
16
  }
17
+ if (options?.transientStatusCodes !== undefined) {
18
+ this.transientStatusCodes = options.transientStatusCodes;
19
+ }
14
20
  }
15
21
 
22
+ readonly transientStatusCodes: ReadonlySet<number> = new Set([
23
+ 408, // Request Timeout
24
+ 429, // Too Many Requests
25
+ 500, // Internal Server Error
26
+ 502, // Bad Gateway
27
+ 503, // Service Unavailable
28
+ 504, // Gateway Timeout
29
+ 522, // Cloudflare Origin Server Connection Timed Out
30
+ ]);
31
+
16
32
  shouldRetry(response: Response | null, retryNumber: number): boolean {
17
33
  if (response === null) return false;
18
34
  if (retryNumber > this.maxRetries) return false;
19
- return response.status >= 500 && response.status < 600;
35
+ const isTransient = this.transientStatusCodes.has(response.status);
36
+ return isTransient;
20
37
  }
21
38
 
22
- retryDelay(retryNumber: number): Milliseconds {
39
+ retryDelay(retryNumber: number, response?: Response | null): Milliseconds {
40
+ // If we have a Retry-After header, use that
41
+ const retryAfter = response?.headers.get('Retry-After');
42
+ if (retryAfter) {
43
+ const retryAfterSeconds = parseInt(retryAfter, 10);
44
+ if (!isNaN(retryAfterSeconds)) {
45
+ return retryAfterSeconds * 1000;
46
+ }
47
+ }
48
+
23
49
  // Exponential backoff up to 10 seconds
24
50
  return Math.min(500 * 2 ** retryNumber, 10000);
25
51
  }
@@ -7,5 +7,5 @@ export interface RetryConfiguring {
7
7
  error?: unknown,
8
8
  ): boolean;
9
9
 
10
- retryDelay(retryNumber: number): Milliseconds;
10
+ retryDelay(retryNumber: number, response?: Response | null): Milliseconds;
11
11
  }
@@ -65,7 +65,7 @@ export class FetchRetrier implements FetchRetrierInterface {
65
65
 
66
66
  const retryConfig = options?.retryConfig ?? this.retryConfiguration;
67
67
  if (retryConfig.shouldRetry(response, retryNumber)) {
68
- const delay = retryConfig.retryDelay(retryNumber);
68
+ const delay = retryConfig.retryDelay(retryNumber, response);
69
69
  await promisedSleep(delay);
70
70
  this.logRetryEvent(
71
71
  urlString,
@@ -13,22 +13,41 @@ describe('DefaultRetryConfiguration', () => {
13
13
  expect(config.shouldRetry(mockResponse, 3)).to.be.false;
14
14
  });
15
15
 
16
- it('should retry on 5xx status codes', async () => {
17
- const config = new DefaultRetryConfiguration();
18
- const mockResponse = new Response(null, { status: 502 });
19
- expect(config.shouldRetry(mockResponse, 1)).to.be.true;
16
+ it('should retry transient status codes', async () => {
17
+ const transientStatuses = [408, 429, 500, 502, 503, 504];
18
+ const config = new DefaultRetryConfiguration({
19
+ transientStatusCodes: new Set(transientStatuses),
20
+ });
21
+ for (const status of transientStatuses) {
22
+ const mockResponse = new Response(null, { status });
23
+ expect(config.shouldRetry(mockResponse, 1)).to.be.true;
24
+ }
20
25
  });
21
26
 
22
- it('should not retry on non-5xx status codes', async () => {
27
+ it('should not retry non-transient status codes', async () => {
23
28
  const config = new DefaultRetryConfiguration();
24
29
  const mockResponse = new Response(null, { status: 404 });
25
30
  expect(config.shouldRetry(mockResponse, 1)).to.be.false;
26
31
  });
27
32
 
33
+ it('uses Retry-After header if present', async () => {
34
+ const config = new DefaultRetryConfiguration();
35
+ const headers = new Headers();
36
+ headers.append('Retry-After', '3');
37
+ const mockResponse = new Response(null, { status: 503, headers });
38
+ expect(config.retryDelay(0, mockResponse)).to.equal(3000);
39
+ });
40
+
28
41
  it('has exponential backoff delay', async () => {
29
42
  const config = new DefaultRetryConfiguration();
30
43
  expect(config.retryDelay(0)).to.equal(500);
31
44
  expect(config.retryDelay(1)).to.equal(1000);
32
45
  expect(config.retryDelay(2)).to.equal(2000);
33
46
  });
47
+
48
+ it('caps retry delay at 10 seconds', async () => {
49
+ const config = new DefaultRetryConfiguration();
50
+ expect(config.retryDelay(10)).to.equal(10000);
51
+ expect(config.retryDelay(20)).to.equal(10000);
52
+ });
34
53
  });