@internetarchive/fetch-handler 1.0.1 → 1.1.0-webdev-7731.2
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 +5 -5
- 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 +16 -3
- package/dist/src/fetch-handler-interface.js.map +1 -1
- package/dist/src/{ia-fetch-handler.d.ts → fetch-handler.d.ts} +10 -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 +23 -4
- package/src/{ia-fetch-handler.ts → fetch-handler.ts} +27 -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,17 +1,16 @@
|
|
|
1
1
|
import { expect } from '@open-wc/testing';
|
|
2
2
|
import sinon from 'sinon';
|
|
3
|
-
import { FetchRetrier } from '../src/
|
|
3
|
+
import { FetchRetrier } from '../src/fetch-retry/fetch-retrier';
|
|
4
4
|
import { MockAnalyticsHandler } from './mocks/mock-analytics-handler';
|
|
5
|
+
import { MockRetryConfig } from './mocks/mock-retry-config';
|
|
5
6
|
|
|
6
7
|
describe('FetchRetrier', () => {
|
|
7
8
|
let fetchStub: sinon.SinonStub;
|
|
8
|
-
let sleepStub: sinon.SinonStub;
|
|
9
9
|
let analytics: MockAnalyticsHandler;
|
|
10
10
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
analytics = new MockAnalyticsHandler();
|
|
13
13
|
fetchStub = sinon.stub(globalThis, 'fetch');
|
|
14
|
-
sleepStub = sinon.stub().resolves(); // stubbed promisedSleep
|
|
15
14
|
});
|
|
16
15
|
|
|
17
16
|
afterEach(() => {
|
|
@@ -22,30 +21,57 @@ describe('FetchRetrier', () => {
|
|
|
22
21
|
fetchStub.resolves(new Response('ok', { status: 200 }));
|
|
23
22
|
const retrier = new FetchRetrier({
|
|
24
23
|
analyticsHandler: analytics,
|
|
25
|
-
|
|
24
|
+
retryConfiguration: new MockRetryConfig(),
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
const res = await retrier.fetchRetry('https://foo.org/data');
|
|
29
28
|
|
|
30
29
|
expect(res.status).to.equal(200);
|
|
31
30
|
expect(fetchStub.callCount).to.equal(1);
|
|
32
|
-
expect(sleepStub.callCount).to.equal(0);
|
|
33
31
|
expect(analytics.events.length).to.equal(0);
|
|
34
32
|
});
|
|
35
33
|
|
|
34
|
+
it('does not retry on 4xx and logs event', async () => {
|
|
35
|
+
fetchStub.resolves(new Response('forbidden', { status: 403 }));
|
|
36
|
+
const retrier = new FetchRetrier({
|
|
37
|
+
analyticsHandler: analytics,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const res = await retrier.fetchRetry('https://foo.org/403');
|
|
41
|
+
|
|
42
|
+
expect(res.status).to.equal(403);
|
|
43
|
+
expect(fetchStub.callCount).to.equal(1);
|
|
44
|
+
expect(analytics.events[0].action).to.equal('status4xxResponse');
|
|
45
|
+
});
|
|
46
|
+
|
|
36
47
|
it('does not retry on 404 and logs event', async () => {
|
|
37
48
|
fetchStub.resolves(new Response('not found', { status: 404 }));
|
|
38
49
|
const retrier = new FetchRetrier({
|
|
39
50
|
analyticsHandler: analytics,
|
|
40
|
-
sleepFn: sleepStub,
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
const res = await retrier.fetchRetry('https://foo.org/404');
|
|
44
54
|
|
|
45
55
|
expect(res.status).to.equal(404);
|
|
46
56
|
expect(fetchStub.callCount).to.equal(1);
|
|
47
|
-
expect(
|
|
48
|
-
|
|
57
|
+
expect(analytics.events[0].action).to.equal('status4xxResponse');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('retries on 4xx if shouldRetry is true in ApiRequestInit', async () => {
|
|
61
|
+
fetchStub.onCall(0).resolves(new Response('bad request', { status: 400 }));
|
|
62
|
+
fetchStub.onCall(1).resolves(new Response('ok', { status: 200 }));
|
|
63
|
+
|
|
64
|
+
const retrier = new FetchRetrier({
|
|
65
|
+
analyticsHandler: analytics,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const res = await retrier.fetchRetry('https://foo.org/should-retry', {
|
|
69
|
+
retryConfig: new MockRetryConfig(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(res.status).to.equal(200);
|
|
73
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
74
|
+
expect(analytics.events.some(e => e.action === 'retryingFetch')).to.be.true;
|
|
49
75
|
});
|
|
50
76
|
|
|
51
77
|
it('retries on 500 and logs retry/failure events', async () => {
|
|
@@ -55,16 +81,13 @@ describe('FetchRetrier', () => {
|
|
|
55
81
|
|
|
56
82
|
const retrier = new FetchRetrier({
|
|
57
83
|
analyticsHandler: analytics,
|
|
58
|
-
|
|
59
|
-
retryDelay: 1,
|
|
60
|
-
sleepFn: sleepStub,
|
|
84
|
+
retryConfiguration: new MockRetryConfig(),
|
|
61
85
|
});
|
|
62
86
|
|
|
63
87
|
const res = await retrier.fetchRetry('https://foo.org/fail');
|
|
64
88
|
|
|
65
89
|
expect(res.status).to.equal(500);
|
|
66
90
|
expect(fetchStub.callCount).to.equal(3);
|
|
67
|
-
expect(sleepStub.callCount).to.equal(2);
|
|
68
91
|
expect(analytics.events.some(e => e.action === 'retryingFetch')).to.be.true;
|
|
69
92
|
expect(analytics.events.some(e => e.action === 'fetchFailed')).to.be.true;
|
|
70
93
|
});
|
|
@@ -75,16 +98,13 @@ describe('FetchRetrier', () => {
|
|
|
75
98
|
|
|
76
99
|
const retrier = new FetchRetrier({
|
|
77
100
|
analyticsHandler: analytics,
|
|
78
|
-
|
|
79
|
-
retryDelay: 1,
|
|
80
|
-
sleepFn: sleepStub,
|
|
101
|
+
retryConfiguration: new MockRetryConfig(),
|
|
81
102
|
});
|
|
82
103
|
|
|
83
104
|
const res = await retrier.fetchRetry('https://foo.org/retry');
|
|
84
105
|
|
|
85
106
|
expect(res.status).to.equal(200);
|
|
86
107
|
expect(fetchStub.callCount).to.equal(2);
|
|
87
|
-
expect(sleepStub.calledOnce).to.be.true;
|
|
88
108
|
expect(analytics.events.some(e => e.action === 'retryingFetch')).to.be.true;
|
|
89
109
|
});
|
|
90
110
|
|
|
@@ -93,20 +113,17 @@ describe('FetchRetrier', () => {
|
|
|
93
113
|
|
|
94
114
|
const retrier = new FetchRetrier({
|
|
95
115
|
analyticsHandler: analytics,
|
|
96
|
-
|
|
97
|
-
retryDelay: 1,
|
|
98
|
-
sleepFn: sleepStub,
|
|
116
|
+
retryConfiguration: new MockRetryConfig(),
|
|
99
117
|
});
|
|
100
118
|
|
|
101
119
|
try {
|
|
102
120
|
await retrier.fetchRetry('https://foo.org/networkfail');
|
|
103
121
|
throw new Error('Should have thrown');
|
|
104
|
-
} catch (err:
|
|
105
|
-
expect(err.message).to.equal('Boom');
|
|
122
|
+
} catch (err: unknown) {
|
|
123
|
+
expect((err as Error).message).to.equal('Boom');
|
|
106
124
|
}
|
|
107
125
|
|
|
108
|
-
expect(fetchStub.callCount).to.equal(
|
|
109
|
-
expect(sleepStub.callCount).to.equal(1);
|
|
126
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
110
127
|
expect(analytics.events.some(e => e.action === 'fetchFailed')).to.be.true;
|
|
111
128
|
});
|
|
112
129
|
|
|
@@ -116,19 +133,16 @@ describe('FetchRetrier', () => {
|
|
|
116
133
|
|
|
117
134
|
const retrier = new FetchRetrier({
|
|
118
135
|
analyticsHandler: analytics,
|
|
119
|
-
retryCount: 2,
|
|
120
|
-
sleepFn: sleepStub,
|
|
121
136
|
});
|
|
122
137
|
|
|
123
138
|
try {
|
|
124
139
|
await retrier.fetchRetry('https://foo.org/blocked');
|
|
125
140
|
throw new Error('Should have thrown');
|
|
126
|
-
} catch (err:
|
|
141
|
+
} catch (err: unknown) {
|
|
127
142
|
expect(err).to.equal(blockerError);
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
expect(fetchStub.callCount).to.equal(1);
|
|
131
|
-
expect(sleepStub.callCount).to.equal(0);
|
|
132
146
|
expect(
|
|
133
147
|
analytics.events.some(
|
|
134
148
|
e => e.action === 'contentBlockerDetectedNotRetrying',
|
|
@@ -136,38 +150,65 @@ describe('FetchRetrier', () => {
|
|
|
136
150
|
).to.be.true;
|
|
137
151
|
});
|
|
138
152
|
|
|
139
|
-
it('
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
it('sleeps for each retry attempt', async () => {
|
|
154
|
+
const retryConfig = new MockRetryConfig();
|
|
155
|
+
const retryDelaySpy = sinon.spy(retryConfig, 'retryDelay');
|
|
156
|
+
fetchStub.resolves(new Response(null, { status: 500 }));
|
|
142
157
|
|
|
143
158
|
const retrier = new FetchRetrier({
|
|
144
159
|
analyticsHandler: analytics,
|
|
145
|
-
|
|
146
|
-
retryDelay: 1234,
|
|
147
|
-
sleepFn: sleepStub,
|
|
160
|
+
retryConfiguration: retryConfig,
|
|
148
161
|
});
|
|
149
162
|
|
|
150
|
-
const res = await retrier.fetchRetry('https://foo.org/retry-
|
|
163
|
+
const res = await retrier.fetchRetry('https://foo.org/retry-fail');
|
|
151
164
|
|
|
152
|
-
expect(res.status).to.equal(
|
|
153
|
-
expect(
|
|
154
|
-
expect(
|
|
165
|
+
expect(res.status).to.equal(500);
|
|
166
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
167
|
+
expect(retryDelaySpy.callCount).to.equal(2);
|
|
155
168
|
});
|
|
156
169
|
|
|
157
|
-
it('
|
|
158
|
-
fetchStub.resolves(new Response(
|
|
170
|
+
it('does not retry 5xx when NoRetryConfiguration is used', async () => {
|
|
171
|
+
fetchStub.resolves(new Response('server error', { status: 500 }));
|
|
172
|
+
const retrier = new FetchRetrier({
|
|
173
|
+
analyticsHandler: analytics,
|
|
174
|
+
retryConfiguration: new (class {
|
|
175
|
+
shouldRetry() {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
retryDelay() {
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
})(),
|
|
182
|
+
});
|
|
159
183
|
|
|
184
|
+
const res = await retrier.fetchRetry('https://foo.org/no-retry-500');
|
|
185
|
+
expect(res.status).to.equal(500);
|
|
186
|
+
expect(fetchStub.callCount).to.equal(1);
|
|
187
|
+
expect(analytics.events.some(e => e.action === 'fetchFailed')).to.be.true;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('does not retry on error when configuration disables retries', async () => {
|
|
191
|
+
fetchStub.rejects(new Error('Immediate failure'));
|
|
160
192
|
const retrier = new FetchRetrier({
|
|
161
193
|
analyticsHandler: analytics,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
194
|
+
retryConfiguration: new (class {
|
|
195
|
+
shouldRetry() {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
retryDelay() {
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
})(),
|
|
165
202
|
});
|
|
166
203
|
|
|
167
|
-
|
|
204
|
+
try {
|
|
205
|
+
await retrier.fetchRetry('https://foo.org/no-retry-error');
|
|
206
|
+
throw new Error('Should have thrown');
|
|
207
|
+
} catch (err: unknown) {
|
|
208
|
+
expect((err as Error).message).to.equal('Immediate failure');
|
|
209
|
+
}
|
|
168
210
|
|
|
169
|
-
expect(
|
|
170
|
-
expect(
|
|
171
|
-
expect(sleepStub.alwaysCalledWith(300)).to.be.true;
|
|
211
|
+
expect(fetchStub.callCount).to.equal(1);
|
|
212
|
+
expect(analytics.events.some(e => e.action === 'fetchFailed')).to.be.true;
|
|
172
213
|
});
|
|
173
214
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { legacyArgsAsFetchOptions } from '../src/fetch-retry/legacy-args';
|
|
3
|
+
import type { FetchOptions } from '../src/fetch-options';
|
|
4
|
+
|
|
5
|
+
describe('legacyArgsAsFetchOptions', () => {
|
|
6
|
+
it('wraps RequestInit into FetchOptions', () => {
|
|
7
|
+
const init: RequestInit = { method: 'PUT' };
|
|
8
|
+
const fo = legacyArgsAsFetchOptions(init);
|
|
9
|
+
expect(fo).to.deep.equal({ requestInit: init });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('returns FetchOptions unchanged', () => {
|
|
13
|
+
const options: FetchOptions = {
|
|
14
|
+
requestInit: { headers: { foo: 'bar' } },
|
|
15
|
+
};
|
|
16
|
+
const fo = legacyArgsAsFetchOptions(options);
|
|
17
|
+
expect(fo).to.equal(options);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns undefined when no options provided', () => {
|
|
21
|
+
const fo = legacyArgsAsFetchOptions(undefined);
|
|
22
|
+
expect(fo).to.equal(undefined);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FetchOptions } from '../../src/fetch-options';
|
|
2
|
+
import type { FetchRetrierInterface } from '../../src/fetch-retry/fetch-retrier';
|
|
3
|
+
import type { RetryConfiguring } from '../../src/fetch-retry/configuration/retry-configuring';
|
|
4
|
+
import { legacyArgsAsFetchOptions } from '../../src/fetch-retry/legacy-args';
|
|
5
|
+
|
|
6
|
+
export class MockFetchRetrier implements FetchRetrierInterface {
|
|
7
|
+
requestInfo?: RequestInfo;
|
|
8
|
+
init?: RequestInit;
|
|
9
|
+
retries?: number;
|
|
10
|
+
retryConfig?: RetryConfiguring;
|
|
11
|
+
|
|
12
|
+
async fetchRetry(
|
|
13
|
+
request: RequestInfo,
|
|
14
|
+
options?: RequestInit | FetchOptions,
|
|
15
|
+
): Promise<Response> {
|
|
16
|
+
const fetchOptions = legacyArgsAsFetchOptions(options);
|
|
17
|
+
this.init = fetchOptions?.requestInit;
|
|
18
|
+
this.retryConfig = fetchOptions?.retryConfig;
|
|
19
|
+
this.requestInfo = request;
|
|
20
|
+
return new Response(JSON.stringify({ boop: 'snoot' }), { status: 200 });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RetryConfiguring } from '../../src/fetch-retry/configuration/retry-configuring';
|
|
2
|
+
|
|
3
|
+
export class MockRetryConfig implements RetryConfiguring {
|
|
4
|
+
mockRetryCount: number = 2;
|
|
5
|
+
|
|
6
|
+
mockRetryDelay: number = 0;
|
|
7
|
+
|
|
8
|
+
shouldRetry(
|
|
9
|
+
response: Response | null,
|
|
10
|
+
retryNumber: number,
|
|
11
|
+
error?: unknown,
|
|
12
|
+
): boolean {
|
|
13
|
+
return retryNumber < this.mockRetryCount;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
retryDelay(): number {
|
|
17
|
+
return this.mockRetryDelay;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { NoRetryConfiguration } from '../src/fetch-retry/configuration/no-retry-configuration';
|
|
3
|
+
|
|
4
|
+
describe('NoRetryConfiguration', () => {
|
|
5
|
+
it('should not retry', async () => {
|
|
6
|
+
const config = new NoRetryConfiguration();
|
|
7
|
+
expect(config.shouldRetry()).to.be.false;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('has no delay', async () => {
|
|
11
|
+
const config = new NoRetryConfiguration();
|
|
12
|
+
expect(config.retryDelay()).to.equal(0);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect } from '@open-wc/testing';
|
|
2
|
+
import { legacyArgsAsFetchOptions } from '../src/fetch-retry/legacy-args';
|
|
3
|
+
import { MockRetryConfig } from './mocks/mock-retry-config';
|
|
4
|
+
|
|
5
|
+
describe('FetchRetrier Legacy Args', () => {
|
|
6
|
+
it('can convert RequestInit to FetchOptions', async () => {
|
|
7
|
+
const options = legacyArgsAsFetchOptions({ method: 'GET' });
|
|
8
|
+
expect(options).to.deep.equal({
|
|
9
|
+
requestInit: {
|
|
10
|
+
method: 'GET',
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('leaves FetchOptions unchanged', async () => {
|
|
16
|
+
const retryConfig = new MockRetryConfig();
|
|
17
|
+
const options = legacyArgsAsFetchOptions({
|
|
18
|
+
requestInit: { method: 'POST' },
|
|
19
|
+
retryConfig: retryConfig,
|
|
20
|
+
});
|
|
21
|
+
expect(options).to.deep.equal({
|
|
22
|
+
requestInit: {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
},
|
|
25
|
+
retryConfig: retryConfig,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -7,9 +7,11 @@ export default /** @type {import("@web/test-runner").TestRunnerConfig} */ ({
|
|
|
7
7
|
files: 'dist/test/**/*.test.js',
|
|
8
8
|
|
|
9
9
|
/** Resolve bare module imports */
|
|
10
|
-
nodeResolve: {
|
|
11
|
-
|
|
12
|
-
},
|
|
10
|
+
nodeResolve: { exportConditions: ['browser', 'development'] },
|
|
11
|
+
|
|
12
|
+
coverageConfig: { include: ['**/src/**'] },
|
|
13
|
+
|
|
14
|
+
testsFinishTimeout: 5000,
|
|
13
15
|
|
|
14
16
|
/** Filter out lit dev mode logs */
|
|
15
17
|
filterBrowserLogs(log) {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ia-fetch-handler.js","sourceRoot":"","sources":["../../src/ia-fetch-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import { FetchRetrier, FetchRetrierInterface } from './utils/fetch-retrier';\nimport type { FetchHandlerInterface } from './fetch-handler-interface';\n\n/**\n * The FetchHandler adds some common helpers:\n * - retry the request if it fails\n * - add `reCache=1` to the request if it's in the current url so the backend sees it\n * - add convenience method for fetching/decoding an API response by just the path\n */\nexport class IaFetchHandler implements FetchHandlerInterface {\n private iaApiBaseUrl?: string;\n\n private fetchRetrier: FetchRetrierInterface = new FetchRetrier();\n\n private searchParams?: string;\n\n constructor(options?: {\n iaApiBaseUrl?: string;\n fetchRetrier?: FetchRetrierInterface;\n searchParams?: string;\n }) {\n if (options?.iaApiBaseUrl) this.iaApiBaseUrl = options.iaApiBaseUrl;\n if (options?.fetchRetrier) this.fetchRetrier = options.fetchRetrier;\n if (options?.searchParams) {\n this.searchParams = options.searchParams;\n } else {\n this.searchParams = window.location.search;\n }\n }\n\n /** @inheritdoc */\n async fetchIAApiResponse<T>(\n path: string,\n options?: {\n includeCredentials?: boolean;\n },\n ): Promise<T> {\n const url = `${this.iaApiBaseUrl}${path}`;\n return this.fetchApiResponse(url, options);\n }\n\n /** @inheritdoc */\n async fetchApiResponse<T>(\n url: string,\n options?: {\n includeCredentials?: boolean;\n method?: string;\n body?: BodyInit;\n headers?: HeadersInit;\n },\n ): Promise<T> {\n const requestInit: RequestInit = {};\n if (options?.includeCredentials) requestInit.credentials = 'include';\n if (options?.method) requestInit.method = options.method;\n if (options?.body) requestInit.body = options.body;\n if (options?.headers) requestInit.headers = options.headers;\n const response = await this.fetch(url, requestInit);\n const json = await response.json();\n return json as T;\n }\n\n /** @inheritdoc */\n async fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {\n let finalInput = input;\n const urlParams = new URLSearchParams(this.searchParams);\n if (urlParams.get('reCache') === '1') {\n finalInput = this.addSearchParams(input, { reCache: '1' });\n }\n return this.fetchRetrier.fetchRetry(finalInput, init);\n }\n\n /**\n * Since RequestInfo can be either a `Request` or `string`, we need to change\n * the way we add search params to it depending on the input.\n */\n private addSearchParams(\n input: RequestInfo,\n params: Record<string, string>,\n ): RequestInfo {\n const urlString = typeof input === 'string' ? input : input.url;\n const url = new URL(urlString, window.location.href);\n\n for (const [key, value] of Object.entries(params)) {\n url.searchParams.set(key, value);\n }\n\n if (typeof input === 'string') {\n return url.href;\n } else {\n const newRequest = new Request(url.href, input);\n return newRequest;\n }\n }\n}\n"]}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { AnalyticsHandler } from '@internetarchive/analytics-manager';
|
|
2
|
-
import { promisedSleep } from './promised-sleep';
|
|
3
|
-
/** @inheritdoc */
|
|
4
|
-
export class FetchRetrier {
|
|
5
|
-
constructor(options) {
|
|
6
|
-
this.analyticsHandler = new AnalyticsHandler({ enableAnalytics: true });
|
|
7
|
-
this.sleep = promisedSleep; // default sleep function
|
|
8
|
-
this.retryCount = 2;
|
|
9
|
-
this.retryDelay = 1000;
|
|
10
|
-
this.eventCategory = 'offshootFetchRetry';
|
|
11
|
-
if (options === null || options === void 0 ? void 0 : options.analyticsHandler)
|
|
12
|
-
this.analyticsHandler = options.analyticsHandler;
|
|
13
|
-
if (options === null || options === void 0 ? void 0 : options.retryCount)
|
|
14
|
-
this.retryCount = options.retryCount;
|
|
15
|
-
if (options === null || options === void 0 ? void 0 : options.retryDelay)
|
|
16
|
-
this.retryDelay = options.retryDelay;
|
|
17
|
-
if (options === null || options === void 0 ? void 0 : options.sleepFn)
|
|
18
|
-
this.sleep = options.sleepFn; // override if provided
|
|
19
|
-
}
|
|
20
|
-
/** @inheritdoc */
|
|
21
|
-
async fetchRetry(requestInfo, init, retries = this.retryCount) {
|
|
22
|
-
const urlString = typeof requestInfo === 'string' ? requestInfo : requestInfo.url;
|
|
23
|
-
const retryNumber = this.retryCount - retries + 1;
|
|
24
|
-
try {
|
|
25
|
-
const response = await fetch(requestInfo, init);
|
|
26
|
-
if (response.ok)
|
|
27
|
-
return response;
|
|
28
|
-
// don't retry on a 404 since this will never succeed
|
|
29
|
-
if (response.status === 404) {
|
|
30
|
-
this.log404Event(urlString);
|
|
31
|
-
return response;
|
|
32
|
-
}
|
|
33
|
-
if (retries > 0) {
|
|
34
|
-
await this.sleep(this.retryDelay);
|
|
35
|
-
this.logRetryEvent(urlString, retryNumber, response.statusText, response.status);
|
|
36
|
-
return this.fetchRetry(requestInfo, init, retries - 1);
|
|
37
|
-
}
|
|
38
|
-
this.logFailureEvent(urlString, response.status);
|
|
39
|
-
return response;
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
// if a content blocker is detected, log it and don't retry
|
|
43
|
-
if (this.isContentBlockerError(error)) {
|
|
44
|
-
this.logContentBlockingEvent(urlString, error);
|
|
45
|
-
throw error;
|
|
46
|
-
}
|
|
47
|
-
if (retries > 0) {
|
|
48
|
-
await this.sleep(this.retryDelay);
|
|
49
|
-
// intentionally duplicating the error message here since we want something
|
|
50
|
-
// in the status code even though we won't have an actual code
|
|
51
|
-
this.logRetryEvent(urlString, retryNumber, error, error);
|
|
52
|
-
return this.fetchRetry(requestInfo, init, retries - 1);
|
|
53
|
-
}
|
|
54
|
-
this.logFailureEvent(urlString, error);
|
|
55
|
-
throw error;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
isContentBlockerError(error) {
|
|
59
|
-
// all of the content blocker errors are `TypeError`
|
|
60
|
-
if (!(error instanceof TypeError))
|
|
61
|
-
return false;
|
|
62
|
-
const message = error.message.toLowerCase();
|
|
63
|
-
return message.includes('content blocker');
|
|
64
|
-
}
|
|
65
|
-
logRetryEvent(urlString, retryNumber, status, code) {
|
|
66
|
-
this.analyticsHandler.sendEvent({
|
|
67
|
-
category: this.eventCategory,
|
|
68
|
-
action: 'retryingFetch',
|
|
69
|
-
label: `retryNumber: ${retryNumber} / ${this.retryCount}, code: ${code}, status: ${status}, url: ${urlString}`,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
logFailureEvent(urlString, error) {
|
|
73
|
-
this.analyticsHandler.sendEvent({
|
|
74
|
-
category: this.eventCategory,
|
|
75
|
-
action: 'fetchFailed',
|
|
76
|
-
label: `error: ${error}, url: ${urlString}`,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
log404Event(urlString) {
|
|
80
|
-
this.analyticsHandler.sendEvent({
|
|
81
|
-
category: this.eventCategory,
|
|
82
|
-
action: 'status404NotRetrying',
|
|
83
|
-
label: `url: ${urlString}`,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
logContentBlockingEvent(urlString, error) {
|
|
87
|
-
this.analyticsHandler.sendEvent({
|
|
88
|
-
category: this.eventCategory,
|
|
89
|
-
action: 'contentBlockerDetectedNotRetrying',
|
|
90
|
-
label: `error: ${error}, url: ${urlString}`,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
//# sourceMappingURL=fetch-retrier.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"fetch-retrier.js","sourceRoot":"","sources":["../../../src/utils/fetch-retrier.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AnalyticsHandlerInterface } from '@internetarchive/analytics-manager';\nimport { AnalyticsHandler } from '@internetarchive/analytics-manager';\nimport { promisedSleep } from './promised-sleep';\n\n/**\n * A class that retries a fetch request.\n */\nexport interface FetchRetrierInterface {\n /**\n * Execute a fetch with retry.\n *\n * @param requestInfo RequestInfo\n * @param init Optional RequestInit\n * @param retries Optional number of retries to attempt\n * @returns Promise<Response>\n */\n fetchRetry(\n requestInfo: RequestInfo,\n init?: RequestInit,\n retries?: number,\n ): Promise<Response>;\n}\n\n/** @inheritdoc */\nexport class FetchRetrier implements FetchRetrierInterface {\n private analyticsHandler = new AnalyticsHandler({ enableAnalytics: true });\n private sleep = promisedSleep; // default sleep function\n\n private retryCount = 2;\n\n private retryDelay = 1000;\n\n constructor(options?: {\n analyticsHandler?: AnalyticsHandlerInterface;\n retryCount?: number;\n retryDelay?: number;\n sleepFn?: (ms: number) => Promise<void>; // <-- add this!\n }) {\n if (options?.analyticsHandler)\n this.analyticsHandler = options.analyticsHandler;\n if (options?.retryCount) this.retryCount = options.retryCount;\n if (options?.retryDelay) this.retryDelay = options.retryDelay;\n if (options?.sleepFn) this.sleep = options.sleepFn; // override if provided\n }\n\n /** @inheritdoc */\n public async fetchRetry(\n requestInfo: RequestInfo,\n init?: RequestInit,\n retries: number = this.retryCount,\n ): Promise<Response> {\n const urlString =\n typeof requestInfo === 'string' ? requestInfo : requestInfo.url;\n const retryNumber = this.retryCount - retries + 1;\n\n try {\n const response = await fetch(requestInfo, init);\n if (response.ok) return response;\n // don't retry on a 404 since this will never succeed\n if (response.status === 404) {\n this.log404Event(urlString);\n return response;\n }\n if (retries > 0) {\n await this.sleep(this.retryDelay);\n this.logRetryEvent(\n urlString,\n retryNumber,\n response.statusText,\n response.status,\n );\n return this.fetchRetry(requestInfo, init, retries - 1);\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 if (retries > 0) {\n await this.sleep(this.retryDelay);\n // intentionally duplicating the error message here since we want something\n // in the status code even though we won't have an actual code\n this.logRetryEvent(urlString, retryNumber, error, error);\n return this.fetchRetry(requestInfo, init, retries - 1);\n }\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} / ${this.retryCount}, 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 log404Event(urlString: string) {\n this.analyticsHandler.sendEvent({\n category: this.eventCategory,\n action: 'status404NotRetrying',\n label: `url: ${urlString}`,\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,50 +0,0 @@
|
|
|
1
|
-
import { expect } from '@open-wc/testing';
|
|
2
|
-
import { IaFetchHandler } from '../src/ia-fetch-handler';
|
|
3
|
-
class MockFetchRetrier {
|
|
4
|
-
async fetchRetry(requestInfo, init, retries) {
|
|
5
|
-
this.init = init;
|
|
6
|
-
this.requestInfo = requestInfo;
|
|
7
|
-
this.retries = retries;
|
|
8
|
-
return new Response(JSON.stringify({ boop: 'snoot' }), { status: 200 });
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
describe('Fetch Handler', () => {
|
|
12
|
-
describe('fetch', () => {
|
|
13
|
-
it('adds reCache=1 if it is in the current url', async () => {
|
|
14
|
-
const fetchRetrier = new MockFetchRetrier();
|
|
15
|
-
const fetchHandler = new IaFetchHandler({
|
|
16
|
-
fetchRetrier: fetchRetrier,
|
|
17
|
-
searchParams: '?reCache=1',
|
|
18
|
-
});
|
|
19
|
-
await fetchHandler.fetch('https://foo.org/api/v1/snoot');
|
|
20
|
-
expect(fetchRetrier.requestInfo).to.equal('https://foo.org/api/v1/snoot?reCache=1');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
describe('fetchIAApiResponse', () => {
|
|
24
|
-
it('prepends the IA basehost to the url when making a request', async () => {
|
|
25
|
-
const endpoint = '/foo/service/endpoint.php';
|
|
26
|
-
const fetchRetrier = new MockFetchRetrier();
|
|
27
|
-
const fetchHandler = new IaFetchHandler({
|
|
28
|
-
iaApiBaseUrl: 'www.example.com',
|
|
29
|
-
fetchRetrier: fetchRetrier,
|
|
30
|
-
});
|
|
31
|
-
await fetchHandler.fetchIAApiResponse(endpoint);
|
|
32
|
-
expect(fetchRetrier.requestInfo).to.equal('www.example.com/foo/service/endpoint.php');
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
describe('fetchApiResponse', () => {
|
|
36
|
-
it('adds credentials: include if requested', async () => {
|
|
37
|
-
const endpoint = '/foo/service/endpoint.php';
|
|
38
|
-
const fetchRetrier = new MockFetchRetrier();
|
|
39
|
-
const fetchHandler = new IaFetchHandler({
|
|
40
|
-
iaApiBaseUrl: 'www.example.com',
|
|
41
|
-
fetchRetrier: fetchRetrier,
|
|
42
|
-
});
|
|
43
|
-
await fetchHandler.fetchApiResponse(endpoint, {
|
|
44
|
-
includeCredentials: true,
|
|
45
|
-
});
|
|
46
|
-
expect(fetchRetrier.init).to.deep.equal({ credentials: 'include' });
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
//# sourceMappingURL=ia-fetch-handler.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ia-fetch-handler.test.js","sourceRoot":"","sources":["../../test/ia-fetch-handler.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGzD,MAAM,gBAAgB;IAKpB,KAAK,CAAC,UAAU,CACd,WAAwB,EACxB,IAAkB,EAClB,OAAgB;QAEhB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IAC1E,CAAC;CACF;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;QACrB,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;YAC1D,MAAM,YAAY,GAAG,IAAI,gBAAgB,EAAE,CAAC;YAC5C,MAAM,YAAY,GAAG,IAAI,cAAc,CAAC;gBACtC,YAAY,EAAE,YAAY;gBAC1B,YAAY,EAAE,YAAY;aAC3B,CAAC,CAAC;YACH,MAAM,YAAY,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;YACzD,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,KAAK,CACvC,wCAAwC,CACzC,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAClC,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;YACzE,MAAM,QAAQ,GAAG,2BAA2B,CAAC;YAC7C,MAAM,YAAY,GAAG,IAAI,gBAAgB,EAAE,CAAC;YAC5C,MAAM,YAAY,GAAG,IAAI,cAAc,CAAC;gBACtC,YAAY,EAAE,iBAAiB;gBAC/B,YAAY,EAAE,YAAY;aAC3B,CAAC,CAAC;YACH,MAAM,YAAY,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAChD,MAAM,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC,KAAK,CACvC,0CAA0C,CAC3C,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAChC,EAAE,CAAC,wCAAwC,EAAE,KAAK,IAAI,EAAE;YACtD,MAAM,QAAQ,GAAG,2BAA2B,CAAC;YAC7C,MAAM,YAAY,GAAG,IAAI,gBAAgB,EAAE,CAAC;YAC5C,MAAM,YAAY,GAAG,IAAI,cAAc,CAAC;gBACtC,YAAY,EAAE,iBAAiB;gBAC/B,YAAY,EAAE,YAAY;aAC3B,CAAC,CAAC;YACH,MAAM,YAAY,CAAC,gBAAgB,CAAC,QAAQ,EAAE;gBAC5C,kBAAkB,EAAE,IAAI;aACzB,CAAC,CAAC;YACH,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,CAAC;QACtE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { IaFetchHandler } from '../src/ia-fetch-handler';\nimport { FetchRetrierInterface } from '../src/utils/fetch-retrier';\n\nclass MockFetchRetrier implements FetchRetrierInterface {\n requestInfo?: RequestInfo;\n init?: RequestInit;\n retries?: number;\n\n async fetchRetry(\n requestInfo: RequestInfo,\n init?: RequestInit,\n retries?: number,\n ): Promise<Response> {\n this.init = init;\n this.requestInfo = requestInfo;\n this.retries = retries;\n return new Response(JSON.stringify({ boop: 'snoot' }), { status: 200 });\n }\n}\n\ndescribe('Fetch Handler', () => {\n describe('fetch', () => {\n it('adds reCache=1 if it is in the current url', async () => {\n const fetchRetrier = new MockFetchRetrier();\n const fetchHandler = new IaFetchHandler({\n fetchRetrier: fetchRetrier,\n searchParams: '?reCache=1',\n });\n await fetchHandler.fetch('https://foo.org/api/v1/snoot');\n expect(fetchRetrier.requestInfo).to.equal(\n 'https://foo.org/api/v1/snoot?reCache=1',\n );\n });\n });\n\n describe('fetchIAApiResponse', () => {\n it('prepends the IA basehost to the url when making a request', async () => {\n const endpoint = '/foo/service/endpoint.php';\n const fetchRetrier = new MockFetchRetrier();\n const fetchHandler = new IaFetchHandler({\n iaApiBaseUrl: 'www.example.com',\n fetchRetrier: fetchRetrier,\n });\n await fetchHandler.fetchIAApiResponse(endpoint);\n expect(fetchRetrier.requestInfo).to.equal(\n 'www.example.com/foo/service/endpoint.php',\n );\n });\n });\n\n describe('fetchApiResponse', () => {\n it('adds credentials: include if requested', async () => {\n const endpoint = '/foo/service/endpoint.php';\n const fetchRetrier = new MockFetchRetrier();\n const fetchHandler = new IaFetchHandler({\n iaApiBaseUrl: 'www.example.com',\n fetchRetrier: fetchRetrier,\n });\n await fetchHandler.fetchApiResponse(endpoint, {\n includeCredentials: true,\n });\n expect(fetchRetrier.init).to.deep.equal({ credentials: 'include' });\n });\n });\n});\n"]}
|