Skip to content

Commit 7b62403

Browse files
ykztsHaroenv
andauthored
feat(fetch-requester): add @algolia/requester-fetch (#1411)
* feat(fetch-requester): add `@algolia/requester-fetch` * chore: unpin cross-fetch Co-authored-by: Haroen Viaene <fingebimus@me.com>
1 parent 7f201f3 commit 7b62403

11 files changed

Lines changed: 428 additions & 1 deletion

File tree

‎.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');
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+
importcrossFetchfrom'cross-fetch';
3+
importnockfrom'nock';
4+
// @ts-ignore
5+
import{Readable}from'readable-stream';
6+
7+
import{createFetchRequester}from'../..';
8+
9+
constoriginalFetch=window.fetch;
10+
11+
beforeEach(()=>{
12+
window.fetch=crossFetch;
13+
});
14+
15+
afterEach(()=>{
16+
window.fetch=originalFetch;
17+
});
18+
19+
constrequester=createFetchRequester();
20+
21+
constheaders={
22+
'content-type': 'application/x-www-form-urlencoded',
23+
};
24+
25+
consttimeoutRequest: Request={
26+
url: 'missing-url-here',
27+
data: '',
28+
headers: {},
29+
method: 'GET',
30+
responseTimeout: 5,
31+
connectTimeout: 2,
32+
};
33+
34+
constrequestStub: 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+
constbody=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+
constresponse=awaitrequester.send(requestStub);
55+
56+
expect(response.content).toEqual(JSON.stringify({foo: 'bar'}));
57+
});
58+
59+
it('resolves status 200',async()=>{
60+
constbody=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+
constresponse=awaitrequester.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+
constreason='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+
constresponse=awaitrequester.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+
constbody={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+
constresponse=awaitrequester.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+
consttestdata=Buffer.from('äöü');
106+
107+
// create a test response stream that is chunked inside a unicode character
108+
function*generate(){
109+
yieldtestdata.slice(0,3);
110+
yieldtestdata.slice(3);
111+
}
112+
113+
consttestStream=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+
constresponse=awaitrequester.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+
constbefore=Date.now();
129+
constresponse=awaitrequester.send({
130+
...timeoutRequest,
131+
...{connectTimeout: 1,url: 'http://www.google.com:81'},
132+
});
133+
134+
constnow=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+
constbefore=Date.now();
143+
constresponse=awaitrequester.send({
144+
...timeoutRequest,
145+
...{connectTimeout: 2,url: 'http://www.google.com:81'},
146+
});
147+
148+
constnow=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+
constbefore=Date.now();
157+
158+
constresponse=awaitrequester.send({
159+
...timeoutRequest,
160+
...{responseTimeout: 2,url: 'http://localhost:1111/'},
161+
});
162+
163+
constnow=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+
constbefore=Date.now();
172+
constresponse=awaitrequester.send({
173+
...timeoutRequest,
174+
...{
175+
responseTimeout: 3,
176+
url: 'http://localhost:1111',
177+
},
178+
});
179+
180+
constnow=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+
constrequest=Object.assign({},requestStub);
189+
constbefore=Date.now();
190+
constresponse=awaitrequester.send({
191+
...request,
192+
url: 'http://localhost:1111',
193+
responseTimeout: 6,// the fake server sleeps for 5 seconds...
194+
});
195+
196+
constnow=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+
constrequest={
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+
constresponse=awaitrequester.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+
constresponse=awaitrequester.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+
constbody=JSON.stringify({foo: 'bar'});
247+
constrequesterTmp=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+
constresponse=awaitrequesterTmp.send(requestStub);
266+
267+
expect(response.status).toBe(200);
268+
expect(response.content).toBe(body);
269+
});
270+
});

0 commit comments

Comments
 (0)