Skip to content

Commit

Permalink
feat(fetch-requester): add @algolia/requester-fetch (#1411)
Browse files Browse the repository at this point in the history
* feat(fetch-requester): add `@algolia/requester-fetch`

* chore: unpin cross-fetch

Co-authored-by: Haroen Viaene <[email protected]>
  • Loading branch information
ykzts and Haroenv authored Jul 6, 2022
1 parent 7f201f3 commit 7b62403
Show file tree
Hide file tree
Showing 11 changed files with 428 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ module.exports = {
['@algolia/recommend', './packages/recommend/src'],
['@algolia/requester-browser-xhr', './packages/requester-browser-xhr/src'],
['@algolia/requester-common', './packages/requester-common/src'],
['@algolia/requester-fetch', './packages/requester-fetch/src'],
['@algolia/requester-node-http', './packages/requester-node-http/src'],
['@algolia/transporter', './packages/transporter/src'],
],
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ module.exports = {
testEnvironment: 'node',
testPathIgnorePatterns: [
'packages/requester-browser-xhr/*',
'packages/requester-fetch/*',
'packages/cache-browser-local-storage/*',
'packages/algoliasearch/src/__tests__/lite.test.ts',
],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@wdio/static-server-service": "5.16.10",
"barrelsby": "2.2.0",
"bundlesize": "0.18.0",
"cross-fetch": "3.1.5",
"dotenv": "8.2.0",
"eslint": "6.8.0",
"eslint-config-algolia": "15.0.0",
Expand Down
7 changes: 7 additions & 0 deletions packages/requester-fetch/api-extractor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../api-extractor.json",
"mainEntryPointFilePath": "./dist/packages/<unscopedPackageName>/src/index.d.ts",
"dtsRollup": {
"untrimmedFilePath": "./dist/<unscopedPackageName>.d.ts"
}
}
2 changes: 2 additions & 0 deletions packages/requester-fetch/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// eslint-disable-next-line functional/immutable-data, import/no-commonjs
module.exports = require('./dist/requester-fetch.cjs.js');
22 changes: 22 additions & 0 deletions packages/requester-fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@algolia/requester-fetch",
"version": "4.13.1",
"private": false,
"description": "Promise-based request library for Fetch.",
"repository": {
"type": "git",
"url": "git://github.com/algolia/algoliasearch-client-javascript.git"
},
"license": "MIT",
"sideEffects": false,
"main": "index.js",
"module": "dist/requester-fetch.esm.js",
"types": "dist/requester-fetch.d.ts",
"files": [
"index.js",
"dist"
],
"dependencies": {
"@algolia/requester-common": "4.13.1"
}
}
270 changes: 270 additions & 0 deletions packages/requester-fetch/src/__tests__/unit/fetch-requester.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import { MethodEnum, Request } from '@algolia/requester-common';
import crossFetch from 'cross-fetch';
import nock from 'nock';
// @ts-ignore
import { Readable } from 'readable-stream';

import { createFetchRequester } from '../..';

const originalFetch = window.fetch;

beforeEach(() => {
window.fetch = crossFetch;
});

afterEach(() => {
window.fetch = originalFetch;
});

const requester = createFetchRequester();

const headers = {
'content-type': 'application/x-www-form-urlencoded',
};

const timeoutRequest: Request = {
url: 'missing-url-here',
data: '',
headers: {},
method: 'GET',
responseTimeout: 5,
connectTimeout: 2,
};

const requestStub: Request = {
url: 'https://algolia-dns.net/foo?x-algolia-header=foo',
method: MethodEnum.Post,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: JSON.stringify({ foo: 'bar' }),
responseTimeout: 2,
connectTimeout: 1,
};

describe('status code handling', () => {
it('sends requests', async () => {
const body = JSON.stringify({ foo: 'bar' });

nock('https://algolia-dns.net', { reqheaders: headers })
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.reply(200, body);

const response = await requester.send(requestStub);

expect(response.content).toEqual(JSON.stringify({ foo: 'bar' }));
});

it('resolves status 200', async () => {
const body = JSON.stringify({ foo: 'bar' });

nock('https://algolia-dns.net', { reqheaders: headers })
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.reply(200, body);

const response = await requester.send(requestStub);

expect(response.status).toBe(200);
expect(response.content).toBe(body);
expect(response.isTimedOut).toBe(false);
});

it('resolves status 300', async () => {
const reason = 'Multiple Choices';

nock('https://algolia-dns.net', { reqheaders: headers })
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.reply(300, reason);

const response = await requester.send(requestStub);

expect(response.status).toBe(300);
expect(response.content).toBe(reason);
expect(response.isTimedOut).toBe(false);
});

it('resolves status 400', async () => {
const body = { message: 'Invalid Application-Id or API-Key' };

nock('https://algolia-dns.net', { reqheaders: headers })
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.reply(400, JSON.stringify(body));

const response = await requester.send(requestStub);

expect(response.status).toBe(400);
expect(response.content).toBe(JSON.stringify(body));
expect(response.isTimedOut).toBe(false);
});

it('handles chunked responses inside unicode character boundaries', async () => {
const testdata = Buffer.from('äöü');

// create a test response stream that is chunked inside a unicode character
function* generate() {
yield testdata.slice(0, 3);
yield testdata.slice(3);
}

const testStream = Readable.from(generate());

nock('https://algolia-dns.net', { reqheaders: headers })
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.reply(200, testStream);

const response = await requester.send(requestStub);

expect(response.content).toEqual(testdata.toString());
});
});

describe('timeout handling', () => {
it('timeouts with the given 1 seconds connection timeout', async () => {
const before = Date.now();
const response = await requester.send({
...timeoutRequest,
...{ connectTimeout: 1, url: 'http://www.google.com:81' },
});

const now = Date.now();

expect(response.content).toBe('Connection timeout');
expect(now - before).toBeGreaterThan(999);
expect(now - before).toBeLessThan(1200);
});

it('connection timeouts with the given 2 seconds connection timeout', async () => {
const before = Date.now();
const response = await requester.send({
...timeoutRequest,
...{ connectTimeout: 2, url: 'http://www.google.com:81' },
});

const now = Date.now();

expect(response.content).toBe('Connection timeout');
expect(now - before).toBeGreaterThan(1999);
expect(now - before).toBeLessThan(2200);
});

it('socket timeouts if response dont appears before the timeout with 2 seconds timeout', async () => {
const before = Date.now();

const response = await requester.send({
...timeoutRequest,
...{ responseTimeout: 2, url: 'http://localhost:1111/' },
});

const now = Date.now();

expect(now - before).toBeGreaterThan(1999);
expect(now - before).toBeLessThan(2200);
expect(response.content).toBe('Socket timeout');
});

it('socket timeouts if response dont appears before the timeout with 3 seconds timeout', async () => {
const before = Date.now();
const response = await requester.send({
...timeoutRequest,
...{
responseTimeout: 3,
url: 'http://localhost:1111',
},
});

const now = Date.now();

expect(response.content).toBe('Socket timeout');
expect(now - before).toBeGreaterThan(2999);
expect(now - before).toBeLessThan(3200);
});

it('do not timeouts if response appears before the timeout', async () => {
const request = Object.assign({}, requestStub);
const before = Date.now();
const response = await requester.send({
...request,
url: 'http://localhost:1111',
responseTimeout: 6, // the fake server sleeps for 5 seconds...
});

const now = Date.now();

expect(response.isTimedOut).toBe(false);
expect(response.status).toBe(200);
expect(response.content).toBe('{"foo": "bar"}');
expect(now - before).toBeGreaterThan(4999);
expect(now - before).toBeLessThan(5200);
});
});

describe('error handling', (): void => {
it('resolves dns not found', async () => {
const request = {
url: 'https://this-dont-exist.algolia.com',
method: MethodEnum.Post,
headers: {
'X-Algolia-Application-Id': 'ABCDE',
'X-Algolia-API-Key': '12345',
'Content-Type': 'application/x-www-form-urlencoded',
},
data: JSON.stringify({ foo: 'bar' }),
responseTimeout: 2,
connectTimeout: 1,
};

const response = await requester.send(request);

expect(response.status).toBe(0);
expect(response.content).toContain('');
expect(response.isTimedOut).toBe(false);
});

it('resolves general network errors', async () => {
nock('https://algolia-dns.net', { reqheaders: headers })
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.replyWithError('This is a general error');

const response = await requester.send(requestStub);

expect(response.status).toBe(0);
expect(response.content).toBe(
'request to https://algolia-dns.net/foo?x-algolia-header=foo failed, reason: This is a general error'
);
expect(response.isTimedOut).toBe(false);
});
});

describe('requesterOptions', () => {
it('allows to pass requesterOptions', async () => {
const body = JSON.stringify({ foo: 'bar' });
const requesterTmp = createFetchRequester({
requesterOptions: {
headers: {
'x-algolia-foo': 'bar',
},
},
});

nock('https://algolia-dns.net', {
reqheaders: {
...headers,
'x-algolia-foo': 'bar',
},
})
.post('/foo')
.query({ 'x-algolia-header': 'foo' })
.reply(200, body);

const response = await requesterTmp.send(requestStub);

expect(response.status).toBe(200);
expect(response.content).toBe(body);
});
});
Loading

0 comments on commit 7b62403

Please sign in to comment.