@internetarchive/fetch-handler 1.0.1 → 1.1.0-webdev-7731.1
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/.github/workflows/ci.yml +5 -0
- package/.github/workflows/gh-pages-main.yml +4 -0
- package/.github/workflows/pr-preview.yml +4 -0
- package/README.md +3 -3
- package/demo/app-root.ts +1 -1
- package/dist/demo/app-root.d.ts +1 -1
- package/dist/demo/app-root.js +1 -1
- package/dist/demo/app-root.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/src/fetch-handler-interface.d.ts +7 -3
- package/dist/src/fetch-handler-interface.js.map +1 -1
- package/dist/src/{ia-fetch-handler.d.ts → fetch-handler.d.ts} +7 -2
- package/dist/src/{ia-fetch-handler.js → fetch-handler.js} +13 -16
- package/dist/src/fetch-handler.js.map +1 -0
- package/dist/src/fetch-options.d.ts +5 -0
- package/dist/src/fetch-options.js +2 -0
- package/dist/src/fetch-options.js.map +1 -0
- package/dist/src/fetch-retry/configuration/default-retry-configuration.d.ts +10 -0
- package/dist/src/fetch-retry/configuration/default-retry-configuration.js +20 -0
- package/dist/src/fetch-retry/configuration/default-retry-configuration.js.map +1 -0
- package/dist/src/fetch-retry/configuration/milliseconds.d.ts +1 -0
- package/dist/src/fetch-retry/configuration/milliseconds.js +2 -0
- package/dist/src/fetch-retry/configuration/milliseconds.js.map +1 -0
- package/dist/src/fetch-retry/configuration/no-retry-configuration.d.ts +6 -0
- package/dist/src/fetch-retry/configuration/no-retry-configuration.js +9 -0
- package/dist/src/fetch-retry/configuration/no-retry-configuration.js.map +1 -0
- package/dist/src/fetch-retry/configuration/retry-configuring.d.ts +5 -0
- package/dist/src/fetch-retry/configuration/retry-configuring.js +2 -0
- package/dist/src/fetch-retry/configuration/retry-configuring.js.map +1 -0
- package/dist/src/{utils → fetch-retry}/fetch-retrier.d.ts +11 -13
- package/dist/src/fetch-retry/fetch-retrier.js +97 -0
- package/dist/src/fetch-retry/fetch-retrier.js.map +1 -0
- package/dist/src/fetch-retry/legacy-args.d.ts +2 -0
- package/dist/src/fetch-retry/legacy-args.js +11 -0
- package/dist/src/fetch-retry/legacy-args.js.map +1 -0
- package/dist/test/default-retry-config.test.js +30 -0
- package/dist/test/default-retry-config.test.js.map +1 -0
- package/dist/test/fetch-handler.test.d.ts +1 -0
- package/dist/test/fetch-handler.test.js +87 -0
- package/dist/test/fetch-handler.test.js.map +1 -0
- package/dist/test/fetch-retrier.test.js +76 -42
- package/dist/test/fetch-retrier.test.js.map +1 -1
- package/dist/test/legacy-args.test.d.ts +1 -0
- package/dist/test/legacy-args.test.js +21 -0
- package/dist/test/legacy-args.test.js.map +1 -0
- package/dist/test/mocks/mock-fetch-retrier.d.ts +10 -0
- package/dist/test/mocks/mock-fetch-retrier.js +11 -0
- package/dist/test/mocks/mock-fetch-retrier.js.map +1 -0
- package/dist/test/mocks/mock-retry-config.d.ts +7 -0
- package/dist/test/mocks/mock-retry-config.js +13 -0
- package/dist/test/mocks/mock-retry-config.js.map +1 -0
- package/dist/test/no-retry-config.test.d.ts +1 -0
- package/dist/test/no-retry-config.test.js +13 -0
- package/dist/test/no-retry-config.test.js.map +1 -0
- package/dist/test/retrier-legacy-args.test.d.ts +1 -0
- package/dist/test/retrier-legacy-args.test.js +27 -0
- package/dist/test/retrier-legacy-args.test.js.map +1 -0
- package/index.ts +9 -1
- package/package.json +5 -5
- package/src/fetch-handler-interface.ts +11 -4
- package/src/{ia-fetch-handler.ts → fetch-handler.ts} +24 -15
- package/src/fetch-options.ts +6 -0
- package/src/fetch-retry/configuration/default-retry-configuration.ts +23 -0
- package/src/fetch-retry/configuration/milliseconds.ts +1 -0
- package/src/fetch-retry/configuration/no-retry-configuration.ts +12 -0
- package/src/fetch-retry/configuration/retry-configuring.ts +11 -0
- package/src/fetch-retry/fetch-retrier.ts +146 -0
- package/src/fetch-retry/legacy-args.ts +13 -0
- package/test/default-retry-config.test.ts +34 -0
- package/test/fetch-handler.test.ts +99 -0
- package/test/fetch-retrier.test.ts +87 -46
- package/test/legacy-args.test.ts +24 -0
- package/test/mocks/mock-fetch-retrier.ts +22 -0
- package/test/mocks/mock-retry-config.ts +19 -0
- package/test/no-retry-config.test.ts +14 -0
- package/test/retrier-legacy-args.test.ts +28 -0
- package/web-test-runner.config.mjs +5 -3
- package/dist/src/ia-fetch-handler.js.map +0 -1
- package/dist/src/utils/fetch-retrier.js +0 -94
- package/dist/src/utils/fetch-retrier.js.map +0 -1
- package/dist/test/ia-fetch-handler.test.js +0 -50
- package/dist/test/ia-fetch-handler.test.js.map +0 -1
- package/src/utils/fetch-retrier.ts +0 -141
- package/test/ia-fetch-handler.test.ts +0 -66
- /package/dist/test/{ia-fetch-handler.test.d.ts → default-retry-config.test.d.ts} +0 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import type { FetchOptions } from './fetch-options';
|
|
2
|
+
import type { RetryConfiguring } from './fetch-retry/configuration/retry-configuring';
|
|
3
|
+
|
|
1
4
|
export interface FetchHandlerInterface {
|
|
2
5
|
/**
|
|
3
6
|
* Generic fetch function that handles retries and common IA parameters like `reCache=1`
|
|
4
7
|
*
|
|
5
8
|
* @param input RequestInfo
|
|
6
|
-
* @param
|
|
9
|
+
* @param options RequestInit | FetchOptions
|
|
7
10
|
*/
|
|
8
|
-
fetch(
|
|
11
|
+
fetch(
|
|
12
|
+
request: RequestInfo,
|
|
13
|
+
options?: RequestInit | FetchOptions,
|
|
14
|
+
): Promise<Response>;
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* A helper function to fetch a response from an API and get a JSON object
|
|
@@ -20,6 +26,7 @@ export interface FetchHandlerInterface {
|
|
|
20
26
|
method?: string;
|
|
21
27
|
body?: BodyInit;
|
|
22
28
|
headers?: HeadersInit;
|
|
29
|
+
retryConfig?: RetryConfiguring;
|
|
23
30
|
},
|
|
24
31
|
): Promise<T>;
|
|
25
32
|
|
|
@@ -30,10 +37,10 @@ export interface FetchHandlerInterface {
|
|
|
30
37
|
* of the full URL. If you need a full URL, use `fetchApiResponse` instead.
|
|
31
38
|
*
|
|
32
39
|
* @param path string
|
|
33
|
-
* @param options?: { includeCredentials?: boolean }
|
|
40
|
+
* @param options?: { includeCredentials?: boolean, retryConfig?: RetryConfiguring }
|
|
34
41
|
*/
|
|
35
42
|
fetchIAApiResponse<T>(
|
|
36
43
|
path: string,
|
|
37
|
-
options?: { includeCredentials?: boolean },
|
|
44
|
+
options?: { includeCredentials?: boolean; retryConfig?: RetryConfiguring },
|
|
38
45
|
): Promise<T>;
|
|
39
46
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FetchRetrier,
|
|
3
|
+
FetchRetrierInterface,
|
|
4
|
+
} from './fetch-retry/fetch-retrier';
|
|
2
5
|
import type { FetchHandlerInterface } from './fetch-handler-interface';
|
|
6
|
+
import type { FetchOptions } from './fetch-options';
|
|
7
|
+
import type { RetryConfiguring } from './fetch-retry/configuration/retry-configuring';
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* The FetchHandler adds some common helpers:
|
|
@@ -18,6 +23,7 @@ export class IaFetchHandler implements FetchHandlerInterface {
|
|
|
18
23
|
iaApiBaseUrl?: string;
|
|
19
24
|
fetchRetrier?: FetchRetrierInterface;
|
|
20
25
|
searchParams?: string;
|
|
26
|
+
defaultRetryConfiguration?: RetryConfiguring;
|
|
21
27
|
}) {
|
|
22
28
|
if (options?.iaApiBaseUrl) this.iaApiBaseUrl = options.iaApiBaseUrl;
|
|
23
29
|
if (options?.fetchRetrier) this.fetchRetrier = options.fetchRetrier;
|
|
@@ -33,6 +39,7 @@ export class IaFetchHandler implements FetchHandlerInterface {
|
|
|
33
39
|
path: string,
|
|
34
40
|
options?: {
|
|
35
41
|
includeCredentials?: boolean;
|
|
42
|
+
retryConfig?: RetryConfiguring;
|
|
36
43
|
},
|
|
37
44
|
): Promise<T> {
|
|
38
45
|
const url = `${this.iaApiBaseUrl}${path}`;
|
|
@@ -47,6 +54,7 @@ export class IaFetchHandler implements FetchHandlerInterface {
|
|
|
47
54
|
method?: string;
|
|
48
55
|
body?: BodyInit;
|
|
49
56
|
headers?: HeadersInit;
|
|
57
|
+
retryConfig?: RetryConfiguring;
|
|
50
58
|
},
|
|
51
59
|
): Promise<T> {
|
|
52
60
|
const requestInit: RequestInit = {};
|
|
@@ -54,19 +62,26 @@ export class IaFetchHandler implements FetchHandlerInterface {
|
|
|
54
62
|
if (options?.method) requestInit.method = options.method;
|
|
55
63
|
if (options?.body) requestInit.body = options.body;
|
|
56
64
|
if (options?.headers) requestInit.headers = options.headers;
|
|
57
|
-
const response = await this.fetch(url,
|
|
65
|
+
const response = await this.fetch(url, {
|
|
66
|
+
requestInit: requestInit,
|
|
67
|
+
retryConfig: options?.retryConfig,
|
|
68
|
+
});
|
|
58
69
|
const json = await response.json();
|
|
59
70
|
return json as T;
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
/** @inheritdoc */
|
|
63
|
-
async fetch(
|
|
64
|
-
|
|
74
|
+
async fetch(
|
|
75
|
+
request: RequestInfo,
|
|
76
|
+
options?: RequestInit | FetchOptions,
|
|
77
|
+
): Promise<Response> {
|
|
78
|
+
let finalRequest = request;
|
|
65
79
|
const urlParams = new URLSearchParams(this.searchParams);
|
|
66
80
|
if (urlParams.get('reCache') === '1') {
|
|
67
|
-
|
|
81
|
+
const urlString = typeof request === 'string' ? request : request.url;
|
|
82
|
+
finalRequest = this.addSearchParams(urlString, { reCache: '1' });
|
|
68
83
|
}
|
|
69
|
-
return this.fetchRetrier.fetchRetry(
|
|
84
|
+
return this.fetchRetrier.fetchRetry(finalRequest, options);
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
/**
|
|
@@ -74,21 +89,15 @@ export class IaFetchHandler implements FetchHandlerInterface {
|
|
|
74
89
|
* the way we add search params to it depending on the input.
|
|
75
90
|
*/
|
|
76
91
|
private addSearchParams(
|
|
77
|
-
|
|
92
|
+
urlString: string,
|
|
78
93
|
params: Record<string, string>,
|
|
79
|
-
):
|
|
80
|
-
const urlString = typeof input === 'string' ? input : input.url;
|
|
94
|
+
): string {
|
|
81
95
|
const url = new URL(urlString, window.location.href);
|
|
82
96
|
|
|
83
97
|
for (const [key, value] of Object.entries(params)) {
|
|
84
98
|
url.searchParams.set(key, value);
|
|
85
99
|
}
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
return url.href;
|
|
89
|
-
} else {
|
|
90
|
-
const newRequest = new Request(url.href, input);
|
|
91
|
-
return newRequest;
|
|
92
|
-
}
|
|
101
|
+
return url.href;
|
|
93
102
|
}
|
|
94
103
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RetryConfiguring } from './retry-configuring';
|
|
2
|
+
import type { Milliseconds } from './milliseconds';
|
|
3
|
+
|
|
4
|
+
export class DefaultRetryConfiguration implements RetryConfiguring {
|
|
5
|
+
private readonly maxRetries: Readonly<number> = 2;
|
|
6
|
+
|
|
7
|
+
constructor(options?: { maxRetries?: number }) {
|
|
8
|
+
if (options?.maxRetries !== undefined) {
|
|
9
|
+
this.maxRetries = options.maxRetries;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
shouldRetry(response: Response | null, retryNumber: number): boolean {
|
|
14
|
+
if (response === null) return false;
|
|
15
|
+
if (retryNumber > this.maxRetries) return false;
|
|
16
|
+
return response.status >= 500 && response.status < 600;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
retryDelay(retryNumber: number): Milliseconds {
|
|
20
|
+
// Exponential backoff up to 10 seconds
|
|
21
|
+
return Math.min(500 * 2 ** retryNumber, 10000);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type Milliseconds = number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RetryConfiguring } from './retry-configuring';
|
|
2
|
+
import type { Milliseconds } from './milliseconds';
|
|
3
|
+
|
|
4
|
+
export class NoRetryConfiguration implements RetryConfiguring {
|
|
5
|
+
shouldRetry(): boolean {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
retryDelay(): Milliseconds {
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { AnalyticsHandlerInterface } from '@internetarchive/analytics-manager';
|
|
2
|
+
import { promisedSleep } from '../utils/promised-sleep';
|
|
3
|
+
import { type FetchOptions } from '../fetch-options';
|
|
4
|
+
import { legacyArgsAsFetchOptions } from './legacy-args';
|
|
5
|
+
import { DefaultRetryConfiguration } from './configuration/default-retry-configuration';
|
|
6
|
+
import type { RetryConfiguring } from './configuration/retry-configuring';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A class that retries a fetch request.
|
|
10
|
+
*/
|
|
11
|
+
export interface FetchRetrierInterface {
|
|
12
|
+
/**
|
|
13
|
+
* Execute a fetch with retry.
|
|
14
|
+
*
|
|
15
|
+
* @param request RequestInfo
|
|
16
|
+
* @param options Optional RequestInit | FetchOptions
|
|
17
|
+
* @returns Promise<Response>
|
|
18
|
+
*/
|
|
19
|
+
fetchRetry(
|
|
20
|
+
request: RequestInfo,
|
|
21
|
+
options?: RequestInit | FetchOptions,
|
|
22
|
+
): Promise<Response>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @inheritdoc */
|
|
26
|
+
export class FetchRetrier implements FetchRetrierInterface {
|
|
27
|
+
private analyticsHandler?: AnalyticsHandlerInterface;
|
|
28
|
+
|
|
29
|
+
private retryConfiguration: RetryConfiguring =
|
|
30
|
+
new DefaultRetryConfiguration();
|
|
31
|
+
|
|
32
|
+
constructor(options?: {
|
|
33
|
+
analyticsHandler?: AnalyticsHandlerInterface;
|
|
34
|
+
retryConfiguration?: RetryConfiguring;
|
|
35
|
+
}) {
|
|
36
|
+
if (options?.analyticsHandler)
|
|
37
|
+
this.analyticsHandler = options.analyticsHandler;
|
|
38
|
+
if (options?.retryConfiguration)
|
|
39
|
+
this.retryConfiguration = options.retryConfiguration;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** @inheritdoc */
|
|
43
|
+
public async fetchRetry(
|
|
44
|
+
request: RequestInfo,
|
|
45
|
+
options?: RequestInit | FetchOptions,
|
|
46
|
+
): Promise<Response> {
|
|
47
|
+
const fetchOptions = legacyArgsAsFetchOptions(options);
|
|
48
|
+
return await this.fetchRetryWithOptions(request, 0, fetchOptions);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async fetchRetryWithOptions(
|
|
52
|
+
request: RequestInfo,
|
|
53
|
+
retryNumber: number,
|
|
54
|
+
options?: FetchOptions,
|
|
55
|
+
): Promise<Response> {
|
|
56
|
+
const urlString = typeof request === 'string' ? request : request.url;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(request, options?.requestInit);
|
|
60
|
+
if (response.ok) return response;
|
|
61
|
+
|
|
62
|
+
if (response.status >= 400 && response.status < 500) {
|
|
63
|
+
this.log4xxResponse(response);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const retryConfig = options?.retryConfig ?? this.retryConfiguration;
|
|
67
|
+
if (retryConfig.shouldRetry(response, retryNumber)) {
|
|
68
|
+
const delay = retryConfig.retryDelay(retryNumber);
|
|
69
|
+
await promisedSleep(delay);
|
|
70
|
+
this.logRetryEvent(
|
|
71
|
+
urlString,
|
|
72
|
+
retryNumber,
|
|
73
|
+
response.statusText,
|
|
74
|
+
response.status,
|
|
75
|
+
);
|
|
76
|
+
return this.fetchRetryWithOptions(request, retryNumber + 1, options);
|
|
77
|
+
}
|
|
78
|
+
this.logFailureEvent(urlString, response.status);
|
|
79
|
+
return response;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// if a content blocker is detected, log it and don't retry
|
|
82
|
+
if (this.isContentBlockerError(error)) {
|
|
83
|
+
this.logContentBlockingEvent(urlString, error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const retryConfig = options?.retryConfig ?? this.retryConfiguration;
|
|
88
|
+
if (retryConfig.shouldRetry(null, retryNumber)) {
|
|
89
|
+
const delay = retryConfig.retryDelay(retryNumber);
|
|
90
|
+
await promisedSleep(delay);
|
|
91
|
+
this.logRetryEvent(urlString, retryNumber, error, error);
|
|
92
|
+
return this.fetchRetryWithOptions(request, retryNumber + 1, options);
|
|
93
|
+
}
|
|
94
|
+
this.logFailureEvent(urlString, error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private isContentBlockerError(error: unknown): boolean {
|
|
100
|
+
// all of the content blocker errors are `TypeError`
|
|
101
|
+
if (!(error instanceof TypeError)) return false;
|
|
102
|
+
const message = error.message.toLowerCase();
|
|
103
|
+
return message.includes('content blocker');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private readonly eventCategory = 'offshootFetchRetry';
|
|
107
|
+
|
|
108
|
+
private logRetryEvent(
|
|
109
|
+
urlString: string,
|
|
110
|
+
retryNumber: number,
|
|
111
|
+
status: unknown,
|
|
112
|
+
code: unknown,
|
|
113
|
+
) {
|
|
114
|
+
this.analyticsHandler?.sendEvent({
|
|
115
|
+
category: this.eventCategory,
|
|
116
|
+
action: 'retryingFetch',
|
|
117
|
+
label: `retryNumber: ${retryNumber}, code: ${code}, status: ${status}, url: ${urlString}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private logFailureEvent(urlString: string, error: unknown) {
|
|
122
|
+
this.analyticsHandler?.sendEvent({
|
|
123
|
+
category: this.eventCategory,
|
|
124
|
+
action: 'fetchFailed',
|
|
125
|
+
label: `error: ${error}, url: ${urlString}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private log4xxResponse(response: Response) {
|
|
130
|
+
const status = response.status;
|
|
131
|
+
|
|
132
|
+
this.analyticsHandler?.sendEvent({
|
|
133
|
+
category: this.eventCategory,
|
|
134
|
+
action: `status4xxResponse`,
|
|
135
|
+
label: `http status ${status}, url: ${response.url}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private logContentBlockingEvent(urlString: string, error: unknown) {
|
|
140
|
+
this.analyticsHandler?.sendEvent({
|
|
141
|
+
category: this.eventCategory,
|
|
142
|
+
action: 'contentBlockerDetectedNotRetrying',
|
|
143
|
+
label: `error: ${error}, url: ${urlString}`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FetchOptions } from '../fetch-options';
|
|
2
|
+
|
|
3
|
+
export const legacyArgsAsFetchOptions = (
|
|
4
|
+
options?: RequestInit | FetchOptions,
|
|
5
|
+
): FetchOptions | undefined => {
|
|
6
|
+
if (!options) return undefined;
|
|
7
|
+
// if options is already FetchOptions, return it
|
|
8
|
+
if ('requestInit' in options || 'retryConfig' in options) {
|
|
9
|
+
return options as FetchOptions;
|
|
10
|
+
}
|
|
11
|
+
// otherwise, it's RequestInit
|
|
12
|
+
return { requestInit: options as RequestInit };
|
|
13
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { DefaultRetryConfiguration } from '../src/fetch-retry/configuration/default-retry-configuration';
|
|
3
|
+
|
|
4
|
+
describe('DefaultRetryConfiguration', () => {
|
|
5
|
+
it('should not retry on null response', async () => {
|
|
6
|
+
const config = new DefaultRetryConfiguration();
|
|
7
|
+
expect(config.shouldRetry(null, 1)).to.be.false;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should not retry after max retries exceeded', async () => {
|
|
11
|
+
const config = new DefaultRetryConfiguration({ maxRetries: 2 });
|
|
12
|
+
const mockResponse = new Response(null, { status: 500 });
|
|
13
|
+
expect(config.shouldRetry(mockResponse, 3)).to.be.false;
|
|
14
|
+
});
|
|
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;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should not retry on non-5xx status codes', async () => {
|
|
23
|
+
const config = new DefaultRetryConfiguration();
|
|
24
|
+
const mockResponse = new Response(null, { status: 404 });
|
|
25
|
+
expect(config.shouldRetry(mockResponse, 1)).to.be.false;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('has exponential backoff delay', async () => {
|
|
29
|
+
const config = new DefaultRetryConfiguration();
|
|
30
|
+
expect(config.retryDelay(0)).to.equal(500);
|
|
31
|
+
expect(config.retryDelay(1)).to.equal(1000);
|
|
32
|
+
expect(config.retryDelay(2)).to.equal(2000);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { IaFetchHandler } from '../src/fetch-handler';
|
|
3
|
+
import { MockFetchRetrier } from './mocks/mock-fetch-retrier';
|
|
4
|
+
import { NoRetryConfiguration } from '../src/fetch-retry/configuration/no-retry-configuration';
|
|
5
|
+
|
|
6
|
+
describe('Fetch Handler', () => {
|
|
7
|
+
describe('fetch', () => {
|
|
8
|
+
it('adds reCache=1 if it is in the current url', async () => {
|
|
9
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
10
|
+
const fetchHandler = new IaFetchHandler({
|
|
11
|
+
fetchRetrier: fetchRetrier,
|
|
12
|
+
searchParams: '?reCache=1',
|
|
13
|
+
});
|
|
14
|
+
await fetchHandler.fetch('https://foo.org/api/v1/snoot');
|
|
15
|
+
expect(fetchRetrier.requestInfo).to.equal(
|
|
16
|
+
'https://foo.org/api/v1/snoot?reCache=1',
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('appends reCache=1 when request is a Request object', async () => {
|
|
21
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
22
|
+
const fetchHandler = new IaFetchHandler({
|
|
23
|
+
fetchRetrier,
|
|
24
|
+
searchParams: '?reCache=1',
|
|
25
|
+
});
|
|
26
|
+
const req = new Request('https://foo.org/api/v1/snoot');
|
|
27
|
+
await fetchHandler.fetch(req);
|
|
28
|
+
expect(fetchRetrier.requestInfo).to.equal(
|
|
29
|
+
'https://foo.org/api/v1/snoot?reCache=1',
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('does not append reCache when not present', async () => {
|
|
34
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
35
|
+
const fetchHandler = new IaFetchHandler({
|
|
36
|
+
fetchRetrier,
|
|
37
|
+
searchParams: '?foo=bar',
|
|
38
|
+
});
|
|
39
|
+
await fetchHandler.fetch('https://foo.org/api/v1/snoot');
|
|
40
|
+
expect(fetchRetrier.requestInfo).to.equal('https://foo.org/api/v1/snoot');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('fetchIAApiResponse', () => {
|
|
45
|
+
it('prepends the IA basehost to the url when making a request', async () => {
|
|
46
|
+
const endpoint = '/foo/service/endpoint.php';
|
|
47
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
48
|
+
const fetchHandler = new IaFetchHandler({
|
|
49
|
+
iaApiBaseUrl: 'www.example.com',
|
|
50
|
+
fetchRetrier: fetchRetrier,
|
|
51
|
+
});
|
|
52
|
+
await fetchHandler.fetchIAApiResponse(endpoint);
|
|
53
|
+
expect(fetchRetrier.requestInfo).to.equal(
|
|
54
|
+
'www.example.com/foo/service/endpoint.php',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('fetchApiResponse', () => {
|
|
60
|
+
it('adds credentials: include if requested', async () => {
|
|
61
|
+
const endpoint = '/foo/service/endpoint.php';
|
|
62
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
63
|
+
const fetchHandler = new IaFetchHandler({
|
|
64
|
+
iaApiBaseUrl: 'www.example.com',
|
|
65
|
+
fetchRetrier: fetchRetrier,
|
|
66
|
+
});
|
|
67
|
+
await fetchHandler.fetchApiResponse(endpoint, {
|
|
68
|
+
includeCredentials: true,
|
|
69
|
+
});
|
|
70
|
+
expect(fetchRetrier.init).to.deep.equal({ credentials: 'include' });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('passes method, body, and headers to RequestInit', async () => {
|
|
74
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
75
|
+
const fetchHandler = new IaFetchHandler({ fetchRetrier });
|
|
76
|
+
const body = JSON.stringify({ hello: 'world' });
|
|
77
|
+
await fetchHandler.fetchApiResponse('https://example.org/api', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body,
|
|
80
|
+
headers: { 'x-test': '1', 'content-type': 'application/json' },
|
|
81
|
+
});
|
|
82
|
+
expect(fetchRetrier.init).to.deep.equal({
|
|
83
|
+
method: 'POST',
|
|
84
|
+
body,
|
|
85
|
+
headers: { 'x-test': '1', 'content-type': 'application/json' },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('passes retryConfig through to retrier', async () => {
|
|
90
|
+
const fetchRetrier = new MockFetchRetrier();
|
|
91
|
+
const retryConfig = new NoRetryConfiguration();
|
|
92
|
+
const fetchHandler = new IaFetchHandler({ fetchRetrier });
|
|
93
|
+
await fetchHandler.fetchApiResponse('https://example.org/api', {
|
|
94
|
+
retryConfig,
|
|
95
|
+
});
|
|
96
|
+
expect(fetchRetrier.retryConfig).to.equal(retryConfig);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|