Skip to content

Commit 2542e53

Browse files
authored
feat(fetch-requester): add @algolia/requester-fetch
1 parent 7f201f3 commit 2542e53

File tree

11 files changed

+428
-1
lines changed

11 files changed

+428
-1
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ module.exports = {
7070
['@algolia/recommend', './packages/recommend/src'],
7171
['@algolia/requester-browser-xhr', './packages/requester-browser-xhr/src'],
7272
['@algolia/requester-common', './packages/requester-common/src'],
73+
['@algolia/requester-fetch', './packages/requester-fetch/src'],
7374
['@algolia/requester-node-http', './packages/requester-node-http/src'],
7475
['@algolia/transporter', './packages/transporter/src'],
7576
],

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ module.exports = {
8383
testEnvironment: 'node',
8484
testPathIgnorePatterns: [
8585
'packages/requester-browser-xhr/*',
86+
'packages/requester-fetch/*',
8687
'packages/cache-browser-local-storage/*',
8788
'packages/algoliasearch/src/__tests__/lite.test.ts',
8889
],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@wdio/static-server-service": "5.16.10",
5151
"barrelsby": "2.2.0",
5252
"bundlesize": "0.18.0",
53+
"cross-fetch": "^3.1.5",
5354
"dotenv": "8.2.0",
5455
"eslint": "6.8.0",
5556
"eslint-config-algolia": "15.0.0",
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../api-extractor.json",
3+
"mainEntryPointFilePath": "./dist/packages/<unscopedPackageName>/src/index.d.ts",
4+
"dtsRollup": {
5+
"untrimmedFilePath": "./dist/<unscopedPackageName>.d.ts"
6+
}
7+
}

packages/requester-fetch/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// eslint-disable-next-line functional/immutable-data, import/no-commonjs
2+
module.exports = require('./dist/requester-fetch.cjs.js');

packages/requester-fetch/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@algolia/requester-fetch",
3+
"version": "4.13.1",
4+
"private": false,
5+
"description": "Promise-based request library for Fetch.",
6+
"repository": {
7+
"type": "git",
8+
"url": "git://github.com/algolia/algoliasearch-client-javascript.git"
9+
},
10+
"license": "MIT",
11+
"sideEffects": false,
12+
"main": "index.js",
13+
"module": "dist/requester-fetch.esm.js",
14+
"types": "dist/requester-fetch.d.ts",
15+
"files": [
16+
"index.js",
17+
"dist"
18+
],
19+
"dependencies": {
20+
"@algolia/requester-common": "4.13.1"
21+
}
22+
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { MethodEnum, Request } from '@algolia/requester-common';
2+
import crossFetch from 'cross-fetch';
3+
import nock from 'nock';
4+
// @ts-ignore
5+
import { Readable } from 'readable-stream';
6+
7+
import { createFetchRequester } from '../..';
8+
9+
const originalFetch = window.fetch;
10+
11+
beforeEach(() => {
12+
window.fetch = crossFetch;
13+
});
14+
15+
afterEach(() => {
16+
window.fetch = originalFetch;
17+
});
18+
19+
const requester = createFetchRequester();
20+
21+
const headers = {
22+
'content-type': 'application/x-www-form-urlencoded',
23+
};
24+
25+
const timeoutRequest: Request = {
26+
url: 'missing-url-here',
27+
data: '',
28+
headers: {},
29+
method: 'GET',
30+
responseTimeout: 5,
31+
connectTimeout: 2,
32+
};
33+
34+
const requestStub: Request = {
35+
url: 'https://algolia-dns.net/foo?x-algolia-header=foo',
36+
method: MethodEnum.Post,
37+
headers: {
38+
'Content-Type': 'application/x-www-form-urlencoded',
39+
},
40+
data: JSON.stringify({ foo: 'bar' }),
41+
responseTimeout: 2,
42+
connectTimeout: 1,
43+
};
44+
45+
describe('status code handling', () => {
46+
it('sends requests', async () => {
47+
const body = JSON.stringify({ foo: 'bar' });
48+
49+
nock('https://algolia-dns.net', { reqheaders: headers })
50+
.post('/foo')
51+
.query({ 'x-algolia-header': 'foo' })
52+
.reply(200, body);
53+
54+
const response = await requester.send(requestStub);
55+
56+
expect(response.content).toEqual(JSON.stringify({ foo: 'bar' }));
57+
});
58+
59+
it('resolves status 200', async () => {
60+
const body = JSON.stringify({ foo: 'bar' });
61+
62+
nock('https://algolia-dns.net', { reqheaders: headers })
63+
.post('/foo')
64+
.query({ 'x-algolia-header': 'foo' })
65+
.reply(200, body);
66+
67+
const response = await requester.send(requestStub);
68+
69+
expect(response.status).toBe(200);
70+
expect(response.content).toBe(body);
71+
expect(response.isTimedOut).toBe(false);
72+
});
73+
74+
it('resolves status 300', async () => {
75+
const reason = 'Multiple Choices';
76+
77+
nock('https://algolia-dns.net', { reqheaders: headers })
78+
.post('/foo')
79+
.query({ 'x-algolia-header': 'foo' })
80+
.reply(300, reason);
81+
82+
const response = await requester.send(requestStub);
83+
84+
expect(response.status).toBe(300);
85+
expect(response.content).toBe(reason);
86+
expect(response.isTimedOut).toBe(false);
87+
});
88+
89+
it('resolves status 400', async () => {
90+
const body = { message: 'Invalid Application-Id or API-Key' };
91+
92+
nock('https://algolia-dns.net', { reqheaders: headers })
93+
.post('/foo')
94+
.query({ 'x-algolia-header': 'foo' })
95+
.reply(400, JSON.stringify(body));
96+
97+
const response = await requester.send(requestStub);
98+
99+
expect(response.status).toBe(400);
100+
expect(response.content).toBe(JSON.stringify(body));
101+
expect(response.isTimedOut).toBe(false);
102+
});
103+
104+
it('handles chunked responses inside unicode character boundaries', async () => {
105+
const testdata = Buffer.from('äöü');
106+
107+
// create a test response stream that is chunked inside a unicode character
108+
function* generate() {
109+
yield testdata.slice(0, 3);
110+
yield testdata.slice(3);
111+
}
112+
113+
const testStream = Readable.from(generate());
114+
115+
nock('https://algolia-dns.net', { reqheaders: headers })
116+
.post('/foo')
117+
.query({ 'x-algolia-header': 'foo' })
118+
.reply(200, testStream);
119+
120+
const response = await requester.send(requestStub);
121+
122+
expect(response.content).toEqual(testdata.toString());
123+
});
124+
});
125+
126+
describe('timeout handling', () => {
127+
it('timeouts with the given 1 seconds connection timeout', async () => {
128+
const before = Date.now();
129+
const response = await requester.send({
130+
...timeoutRequest,
131+
...{ connectTimeout: 1, url: 'http://www.google.com:81' },
132+
});
133+
134+
const now = Date.now();
135+
136+
expect(response.content).toBe('Connection timeout');
137+
expect(now - before).toBeGreaterThan(999);
138+
expect(now - before).toBeLessThan(1200);
139+
});
140+
141+
it('connection timeouts with the given 2 seconds connection timeout', async () => {
142+
const before = Date.now();
143+
const response = await requester.send({
144+
...timeoutRequest,
145+
...{ connectTimeout: 2, url: 'http://www.google.com:81' },
146+
});
147+
148+
const now = Date.now();
149+
150+
expect(response.content).toBe('Connection timeout');
151+
expect(now - before).toBeGreaterThan(1999);
152+
expect(now - before).toBeLessThan(2200);
153+
});
154+
155+
it('socket timeouts if response dont appears before the timeout with 2 seconds timeout', async () => {
156+
const before = Date.now();
157+
158+
const response = await requester.send({
159+
...timeoutRequest,
160+
...{ responseTimeout: 2, url: 'http://localhost:1111/' },
161+
});
162+
163+
const now = Date.now();
164+
165+
expect(now - before).toBeGreaterThan(1999);
166+
expect(now - before).toBeLessThan(2200);
167+
expect(response.content).toBe('Socket timeout');
168+
});
169+
170+
it('socket timeouts if response dont appears before the timeout with 3 seconds timeout', async () => {
171+
const before = Date.now();
172+
const response = await requester.send({
173+
...timeoutRequest,
174+
...{
175+
responseTimeout: 3,
176+
url: 'http://localhost:1111',
177+
},
178+
});
179+
180+
const now = Date.now();
181+
182+
expect(response.content).toBe('Socket timeout');
183+
expect(now - before).toBeGreaterThan(2999);
184+
expect(now - before).toBeLessThan(3200);
185+
});
186+
187+
it('do not timeouts if response appears before the timeout', async () => {
188+
const request = Object.assign({}, requestStub);
189+
const before = Date.now();
190+
const response = await requester.send({
191+
...request,
192+
url: 'http://localhost:1111',
193+
responseTimeout: 6, // the fake server sleeps for 5 seconds...
194+
});
195+
196+
const now = Date.now();
197+
198+
expect(response.isTimedOut).toBe(false);
199+
expect(response.status).toBe(200);
200+
expect(response.content).toBe('{"foo": "bar"}');
201+
expect(now - before).toBeGreaterThan(4999);
202+
expect(now - before).toBeLessThan(5200);
203+
});
204+
});
205+
206+
describe('error handling', (): void => {
207+
it('resolves dns not found', async () => {
208+
const request = {
209+
url: 'https://this-dont-exist.algolia.com',
210+
method: MethodEnum.Post,
211+
headers: {
212+
'X-Algolia-Application-Id': 'ABCDE',
213+
'X-Algolia-API-Key': '12345',
214+
'Content-Type': 'application/x-www-form-urlencoded',
215+
},
216+
data: JSON.stringify({ foo: 'bar' }),
217+
responseTimeout: 2,
218+
connectTimeout: 1,
219+
};
220+
221+
const response = await requester.send(request);
222+
223+
expect(response.status).toBe(0);
224+
expect(response.content).toContain('');
225+
expect(response.isTimedOut).toBe(false);
226+
});
227+
228+
it('resolves general network errors', async () => {
229+
nock('https://algolia-dns.net', { reqheaders: headers })
230+
.post('/foo')
231+
.query({ 'x-algolia-header': 'foo' })
232+
.replyWithError('This is a general error');
233+
234+
const response = await requester.send(requestStub);
235+
236+
expect(response.status).toBe(0);
237+
expect(response.content).toBe(
238+
'request to https://algolia-dns.net/foo?x-algolia-header=foo failed, reason: This is a general error'
239+
);
240+
expect(response.isTimedOut).toBe(false);
241+
});
242+
});
243+
244+
describe('requesterOptions', () => {
245+
it('allows to pass requesterOptions', async () => {
246+
const body = JSON.stringify({ foo: 'bar' });
247+
const requesterTmp = createFetchRequester({
248+
requesterOptions: {
249+
headers: {
250+
'x-algolia-foo': 'bar',
251+
},
252+
},
253+
});
254+
255+
nock('https://algolia-dns.net', {
256+
reqheaders: {
257+
...headers,
258+
'x-algolia-foo': 'bar',
259+
},
260+
})
261+
.post('/foo')
262+
.query({ 'x-algolia-header': 'foo' })
263+
.reply(200, body);
264+
265+
const response = await requesterTmp.send(requestStub);
266+
267+
expect(response.status).toBe(200);
268+
expect(response.content).toBe(body);
269+
});
270+
});

0 commit comments

Comments
 (0)