@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.
- package/dist/src/fetch-retry/configuration/default-retry-configuration.d.ts +3 -1
- package/dist/src/fetch-retry/configuration/default-retry-configuration.js +23 -2
- package/dist/src/fetch-retry/configuration/default-retry-configuration.js.map +1 -1
- package/dist/src/fetch-retry/configuration/retry-configuring.d.ts +1 -1
- package/dist/src/fetch-retry/configuration/retry-configuring.js.map +1 -1
- package/dist/src/fetch-retry/fetch-retrier.js +1 -1
- package/dist/src/fetch-retry/fetch-retrier.js.map +1 -1
- package/dist/test/default-retry-config.test.js +22 -5
- package/dist/test/default-retry-config.test.js.map +1 -1
- package/package.json +1 -1
- package/src/fetch-retry/configuration/default-retry-configuration.ts +29 -3
- package/src/fetch-retry/configuration/retry-configuring.ts +1 -1
- package/src/fetch-retry/fetch-retrier.ts +1 -1
- package/test/default-retry-config.test.ts +24 -5
|
@@ -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
|
-
|
|
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,
|
|
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":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAExD,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,yBAAyB,EAAE,MAAM,6CAA6C,CAAC;AAoBxF,kBAAkB;AAClB,MAAM,OAAO,YAAY;IAMvB,YAAY,OAGX;QANO,uBAAkB,GACxB,IAAI,yBAAyB,EAAE,CAAC;QA4EjB,kBAAa,GAAG,oBAAoB,CAAC;QAtEpD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,gBAAgB;YAC3B,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACnD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,kBAAkB;YAC7B,IAAI,CAAC,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IACzD,CAAC;IAED,kBAAkB;IACX,KAAK,CAAC,UAAU,CACrB,OAAoB,EACpB,OAAoC;QAEpC,MAAM,YAAY,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACvD,OAAO,MAAM,IAAI,CAAC,qBAAqB,CAAC,OAAO,EAAE,CAAC,EAAE,YAAY,CAAC,CAAC;IACpE,CAAC;IAEO,KAAK,CAAC,qBAAqB,CACjC,OAAoB,EACpB,WAAmB,EACnB,OAAsB;;QAEtB,MAAM,SAAS,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QAEtE,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,WAAW,CAAC,CAAC;YAC5D,IAAI,QAAQ,CAAC,EAAE;gBAAE,OAAO,QAAQ,CAAC;YAEjC,IAAI,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;gBACpD,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;YAChC,CAAC;YAED,MAAM,WAAW,GAAG,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,WAAW,mCAAI,IAAI,CAAC,kBAAkB,CAAC;YACpE,IAAI,WAAW,CAAC,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC;gBACnD,MAAM,KAAK,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;
|
|
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
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
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
|
|
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,
|
|
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,19 +7,45 @@ export class DefaultRetryConfiguration implements RetryConfiguring {
|
|
|
7
7
|
|
|
8
8
|
private readonly maxRetries: Readonly<number> = 2;
|
|
9
9
|
|
|
10
|
-
constructor(options?: {
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
|
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
|
});
|