@algolia/requester-node-http 4.14.2 → 5.0.0-alpha.3
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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/requester-node-http.cjs.js +75 -70
- package/dist/requester-node-http.esm.node.js +82 -0
- package/dist/src/createHttpRequester.d.ts +16 -0
- package/dist/src/createHttpRequester.d.ts.map +1 -0
- package/dist/src/echoRequester.d.ts +3 -0
- package/dist/src/echoRequester.d.ts.map +1 -0
- package/index.ts +2 -0
- package/package.json +25 -13
- package/src/__tests__/node-http-requester.test.ts +293 -0
- package/src/createHttpRequester.ts +120 -0
- package/src/echoRequester.ts +8 -0
- package/dist/requester-node-http.d.ts +0 -16
- package/dist/requester-node-http.esm.js +0 -85
- package/index.js +0 -2
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC"}
|
|
@@ -2,86 +2,91 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var url = require('url');
|
|
6
|
+
var clientCommon = require('@algolia/client-common');
|
|
5
7
|
var http = require('http');
|
|
6
8
|
var https = require('https');
|
|
7
|
-
var URL = require('url');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
11
|
+
|
|
12
|
+
var http__default = /*#__PURE__*/_interopDefaultLegacy(http);
|
|
13
|
+
var https__default = /*#__PURE__*/_interopDefaultLegacy(https);
|
|
14
|
+
|
|
15
|
+
function echoRequester(status = 200) {
|
|
16
|
+
return clientCommon.createEchoRequester({ getURL: (url$1) => new url.URL(url$1), status });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Global agents allow us to reuse the TCP protocol with multiple clients
|
|
10
20
|
const agentOptions = { keepAlive: true };
|
|
11
|
-
const defaultHttpAgent = new
|
|
12
|
-
const defaultHttpsAgent = new
|
|
13
|
-
function
|
|
21
|
+
const defaultHttpAgent = new http__default["default"].Agent(agentOptions);
|
|
22
|
+
const defaultHttpsAgent = new https__default["default"].Agent(agentOptions);
|
|
23
|
+
function createHttpRequester({ agent: userGlobalAgent, httpAgent: userHttpAgent, httpsAgent: userHttpsAgent, requesterOptions = {}, } = {}) {
|
|
14
24
|
const httpAgent = userHttpAgent || userGlobalAgent || defaultHttpAgent;
|
|
15
25
|
const httpsAgent = userHttpsAgent || userGlobalAgent || defaultHttpsAgent;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
...
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
clearTimeout(connectTimeout);
|
|
42
|
-
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
43
|
-
clearTimeout(responseTimeout);
|
|
44
|
-
resolve({
|
|
45
|
-
status: response.statusCode || 0,
|
|
46
|
-
content: Buffer.concat(contentBuffers).toString(),
|
|
47
|
-
isTimedOut: false,
|
|
48
|
-
});
|
|
49
|
-
});
|
|
26
|
+
function send(request) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
let responseTimeout;
|
|
29
|
+
// eslint-disable-next-line prefer-const -- linter thinks this is not reassigned
|
|
30
|
+
let connectTimeout;
|
|
31
|
+
const url$1 = new url.URL(request.url);
|
|
32
|
+
const path = url$1.search === null ? url$1.pathname : `${url$1.pathname}${url$1.search}`;
|
|
33
|
+
const options = {
|
|
34
|
+
agent: url$1.protocol === 'https:' ? httpsAgent : httpAgent,
|
|
35
|
+
hostname: url$1.hostname,
|
|
36
|
+
path,
|
|
37
|
+
method: request.method,
|
|
38
|
+
...requesterOptions,
|
|
39
|
+
headers: {
|
|
40
|
+
...request.headers,
|
|
41
|
+
...requesterOptions.headers,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
if (url$1.port && !requesterOptions.port) {
|
|
45
|
+
options.port = url$1.port;
|
|
46
|
+
}
|
|
47
|
+
const req = (url$1.protocol === 'https:' ? https__default["default"] : http__default["default"]).request(options, (response) => {
|
|
48
|
+
let contentBuffers = [];
|
|
49
|
+
response.on('data', (chunk) => {
|
|
50
|
+
contentBuffers = contentBuffers.concat(chunk);
|
|
50
51
|
});
|
|
51
|
-
|
|
52
|
-
return setTimeout(() => {
|
|
53
|
-
req.abort();
|
|
54
|
-
resolve({
|
|
55
|
-
status: 0,
|
|
56
|
-
content,
|
|
57
|
-
isTimedOut: true,
|
|
58
|
-
});
|
|
59
|
-
}, timeout * 1000);
|
|
60
|
-
};
|
|
61
|
-
const connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout');
|
|
62
|
-
// eslint-disable-next-line functional/no-let
|
|
63
|
-
let responseTimeout;
|
|
64
|
-
req.on('error', error => {
|
|
52
|
+
response.on('end', () => {
|
|
65
53
|
clearTimeout(connectTimeout);
|
|
66
54
|
clearTimeout(responseTimeout);
|
|
67
|
-
resolve({
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
resolve({
|
|
56
|
+
status: response.statusCode || 0,
|
|
57
|
+
content: Buffer.concat(contentBuffers).toString(),
|
|
58
|
+
isTimedOut: false,
|
|
59
|
+
});
|
|
72
60
|
});
|
|
73
|
-
if (request.data !== undefined) {
|
|
74
|
-
req.write(request.data);
|
|
75
|
-
}
|
|
76
|
-
req.end();
|
|
77
61
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
62
|
+
const createTimeout = (timeout, content) => {
|
|
63
|
+
return setTimeout(() => {
|
|
64
|
+
req.destroy();
|
|
65
|
+
resolve({
|
|
66
|
+
status: 0,
|
|
67
|
+
content,
|
|
68
|
+
isTimedOut: true,
|
|
69
|
+
});
|
|
70
|
+
}, timeout);
|
|
71
|
+
};
|
|
72
|
+
connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout');
|
|
73
|
+
req.on('error', (error) => {
|
|
74
|
+
clearTimeout(connectTimeout);
|
|
75
|
+
clearTimeout(responseTimeout);
|
|
76
|
+
resolve({ status: 0, content: error.message, isTimedOut: false });
|
|
77
|
+
});
|
|
78
|
+
req.once('response', () => {
|
|
79
|
+
clearTimeout(connectTimeout);
|
|
80
|
+
responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout');
|
|
81
|
+
});
|
|
82
|
+
if (request.data !== undefined) {
|
|
83
|
+
req.write(request.data);
|
|
84
|
+
}
|
|
85
|
+
req.end();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return { send };
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
exports.
|
|
91
|
+
exports.createHttpRequester = createHttpRequester;
|
|
92
|
+
exports.echoRequester = echoRequester;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { URL } from 'url';
|
|
2
|
+
import { createEchoRequester } from '@algolia/client-common';
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
|
|
6
|
+
function echoRequester(status = 200) {
|
|
7
|
+
return createEchoRequester({ getURL: (url) => new URL(url), status });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Global agents allow us to reuse the TCP protocol with multiple clients
|
|
11
|
+
const agentOptions = { keepAlive: true };
|
|
12
|
+
const defaultHttpAgent = new http.Agent(agentOptions);
|
|
13
|
+
const defaultHttpsAgent = new https.Agent(agentOptions);
|
|
14
|
+
function createHttpRequester({ agent: userGlobalAgent, httpAgent: userHttpAgent, httpsAgent: userHttpsAgent, requesterOptions = {}, } = {}) {
|
|
15
|
+
const httpAgent = userHttpAgent || userGlobalAgent || defaultHttpAgent;
|
|
16
|
+
const httpsAgent = userHttpsAgent || userGlobalAgent || defaultHttpsAgent;
|
|
17
|
+
function send(request) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let responseTimeout;
|
|
20
|
+
// eslint-disable-next-line prefer-const -- linter thinks this is not reassigned
|
|
21
|
+
let connectTimeout;
|
|
22
|
+
const url = new URL(request.url);
|
|
23
|
+
const path = url.search === null ? url.pathname : `${url.pathname}${url.search}`;
|
|
24
|
+
const options = {
|
|
25
|
+
agent: url.protocol === 'https:' ? httpsAgent : httpAgent,
|
|
26
|
+
hostname: url.hostname,
|
|
27
|
+
path,
|
|
28
|
+
method: request.method,
|
|
29
|
+
...requesterOptions,
|
|
30
|
+
headers: {
|
|
31
|
+
...request.headers,
|
|
32
|
+
...requesterOptions.headers,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
if (url.port && !requesterOptions.port) {
|
|
36
|
+
options.port = url.port;
|
|
37
|
+
}
|
|
38
|
+
const req = (url.protocol === 'https:' ? https : http).request(options, (response) => {
|
|
39
|
+
let contentBuffers = [];
|
|
40
|
+
response.on('data', (chunk) => {
|
|
41
|
+
contentBuffers = contentBuffers.concat(chunk);
|
|
42
|
+
});
|
|
43
|
+
response.on('end', () => {
|
|
44
|
+
clearTimeout(connectTimeout);
|
|
45
|
+
clearTimeout(responseTimeout);
|
|
46
|
+
resolve({
|
|
47
|
+
status: response.statusCode || 0,
|
|
48
|
+
content: Buffer.concat(contentBuffers).toString(),
|
|
49
|
+
isTimedOut: false,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
const createTimeout = (timeout, content) => {
|
|
54
|
+
return setTimeout(() => {
|
|
55
|
+
req.destroy();
|
|
56
|
+
resolve({
|
|
57
|
+
status: 0,
|
|
58
|
+
content,
|
|
59
|
+
isTimedOut: true,
|
|
60
|
+
});
|
|
61
|
+
}, timeout);
|
|
62
|
+
};
|
|
63
|
+
connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout');
|
|
64
|
+
req.on('error', (error) => {
|
|
65
|
+
clearTimeout(connectTimeout);
|
|
66
|
+
clearTimeout(responseTimeout);
|
|
67
|
+
resolve({ status: 0, content: error.message, isTimedOut: false });
|
|
68
|
+
});
|
|
69
|
+
req.once('response', () => {
|
|
70
|
+
clearTimeout(connectTimeout);
|
|
71
|
+
responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout');
|
|
72
|
+
});
|
|
73
|
+
if (request.data !== undefined) {
|
|
74
|
+
req.write(request.data);
|
|
75
|
+
}
|
|
76
|
+
req.end();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { send };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { createHttpRequester, echoRequester };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import http from 'http';
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import type { Requester } from '@algolia/client-common';
|
|
6
|
+
export declare type CreateHttpRequesterOptions = Partial<{
|
|
7
|
+
agent: http.Agent | https.Agent;
|
|
8
|
+
httpAgent: http.Agent;
|
|
9
|
+
httpsAgent: https.Agent;
|
|
10
|
+
/**
|
|
11
|
+
* RequestOptions to be merged with the end request, it will override default options if provided.
|
|
12
|
+
*/
|
|
13
|
+
requesterOptions: https.RequestOptions;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function createHttpRequester({ agent: userGlobalAgent, httpAgent: userHttpAgent, httpsAgent: userHttpsAgent, requesterOptions, }?: CreateHttpRequesterOptions): Requester;
|
|
16
|
+
//# sourceMappingURL=createHttpRequester.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createHttpRequester.d.ts","sourceRoot":"","sources":["../../../../packages/requester-node-http/src/createHttpRequester.ts"],"names":[],"mappings":";;AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAc,SAAS,EAAY,MAAM,wBAAwB,CAAC;AAE9E,oBAAY,0BAA0B,GAAG,OAAO,CAAC;IAC/C,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAChC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC;IACtB,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC;IACxB;;OAEG;IACH,gBAAgB,EAAE,KAAK,CAAC,cAAc,CAAC;CACxC,CAAC,CAAC;AAOH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EAAE,eAAe,EACtB,SAAS,EAAE,aAAa,EACxB,UAAU,EAAE,cAAc,EAC1B,gBAAqB,GACtB,GAAE,0BAA+B,GAAG,SAAS,CA6F7C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"echoRequester.d.ts","sourceRoot":"","sources":["../../../../packages/requester-node-http/src/echoRequester.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAExD,wBAAgB,aAAa,CAAC,MAAM,GAAE,MAAY,GAAG,SAAS,CAE7D"}
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@algolia/requester-node-http",
|
|
3
|
-
"version": "
|
|
4
|
-
"private": false,
|
|
3
|
+
"version": "5.0.0-alpha.3",
|
|
5
4
|
"description": "Promise-based request library for node using the native http module.",
|
|
6
|
-
"repository":
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git://github.com/algolia/algoliasearch-client-javascript.git"
|
|
9
|
-
},
|
|
5
|
+
"repository": "algolia/algoliasearch-client-javascript",
|
|
10
6
|
"license": "MIT",
|
|
11
|
-
"
|
|
12
|
-
"main": "
|
|
13
|
-
"module": "dist/requester-node-http.esm.js",
|
|
14
|
-
"types": "dist/
|
|
7
|
+
"author": "Algolia",
|
|
8
|
+
"main": "dist/requester-node-http.cjs.js",
|
|
9
|
+
"module": "dist/requester-node-http.esm.node.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
15
11
|
"files": [
|
|
16
|
-
"
|
|
17
|
-
"
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"index.ts"
|
|
18
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"clean": "rm -rf dist/",
|
|
18
|
+
"test": "jest"
|
|
19
|
+
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@algolia/
|
|
21
|
+
"@algolia/client-common": "5.0.0-alpha.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/jest": "28.1.6",
|
|
25
|
+
"@types/node": "16.11.45",
|
|
26
|
+
"jest": "28.1.3",
|
|
27
|
+
"nock": "13.2.9",
|
|
28
|
+
"ts-jest": "28.0.5",
|
|
29
|
+
"typescript": "4.7.4"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">= 14.0.0"
|
|
21
33
|
}
|
|
22
34
|
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { Readable } from 'stream';
|
|
4
|
+
|
|
5
|
+
import type { EndRequest } from '@algolia/client-common';
|
|
6
|
+
import nock from 'nock';
|
|
7
|
+
|
|
8
|
+
import { createHttpRequester } from '../..';
|
|
9
|
+
import {
|
|
10
|
+
headers,
|
|
11
|
+
timeoutRequest,
|
|
12
|
+
requestStub,
|
|
13
|
+
testQueryHeader,
|
|
14
|
+
testQueryBaseUrl,
|
|
15
|
+
getStringifiedBody,
|
|
16
|
+
createTestServer,
|
|
17
|
+
} from '../../../../tests/utils';
|
|
18
|
+
|
|
19
|
+
const requester = createHttpRequester();
|
|
20
|
+
|
|
21
|
+
const httpsBaseRequest = https.request;
|
|
22
|
+
const httpBaseRequest = http.request;
|
|
23
|
+
|
|
24
|
+
describe('api', () => {
|
|
25
|
+
const mockedRequestResponse = {
|
|
26
|
+
destroy: jest.fn(),
|
|
27
|
+
on: jest.fn(),
|
|
28
|
+
once: jest.fn(),
|
|
29
|
+
write: jest.fn(),
|
|
30
|
+
end: jest.fn(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
// @ts-expect-error we don't care about the response for those tests
|
|
35
|
+
https.request = jest.fn(() => mockedRequestResponse);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterAll(() => {
|
|
39
|
+
https.request = httpsBaseRequest;
|
|
40
|
+
http.request = httpBaseRequest;
|
|
41
|
+
jest.resetAllMocks();
|
|
42
|
+
jest.clearAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('allow init without parameters', () => {
|
|
46
|
+
expect(() => createHttpRequester()).not.toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('allow providing custom agent', async () => {
|
|
50
|
+
const agent = new http.Agent();
|
|
51
|
+
// @ts-expect-error we don't care about the response for those tests
|
|
52
|
+
http.request = jest.fn(() => mockedRequestResponse);
|
|
53
|
+
const tmpRequester = createHttpRequester({
|
|
54
|
+
agent,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await tmpRequester.send({
|
|
58
|
+
...requestStub,
|
|
59
|
+
url: 'http://algolia-dns.net/foo?x-algolia-header=bar',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(http.request).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('allow overriding default options', async () => {
|
|
66
|
+
const tmpRequester = createHttpRequester({
|
|
67
|
+
requesterOptions: {
|
|
68
|
+
headers: {
|
|
69
|
+
'my-extra-header': 'algolia',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await tmpRequester.send(requestStub);
|
|
75
|
+
|
|
76
|
+
expect(https.request).toHaveBeenCalledWith(
|
|
77
|
+
expect.objectContaining({
|
|
78
|
+
headers: expect.objectContaining({
|
|
79
|
+
'my-extra-header': 'algolia',
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
expect.any(Function)
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('status code handling', () => {
|
|
88
|
+
it('sends requests', async () => {
|
|
89
|
+
const body = getStringifiedBody();
|
|
90
|
+
|
|
91
|
+
nock(testQueryBaseUrl, { reqheaders: headers })
|
|
92
|
+
.post('/foo')
|
|
93
|
+
.query(testQueryHeader)
|
|
94
|
+
.reply(200, body);
|
|
95
|
+
|
|
96
|
+
const response = await requester.send(requestStub);
|
|
97
|
+
|
|
98
|
+
expect(response.content).toEqual(body);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('resolves status 200', async () => {
|
|
102
|
+
const body = getStringifiedBody();
|
|
103
|
+
|
|
104
|
+
nock(testQueryBaseUrl, { reqheaders: headers })
|
|
105
|
+
.post('/foo')
|
|
106
|
+
.query(testQueryHeader)
|
|
107
|
+
.reply(200, body);
|
|
108
|
+
|
|
109
|
+
const response = await requester.send(requestStub);
|
|
110
|
+
|
|
111
|
+
expect(response.status).toBe(200);
|
|
112
|
+
expect(response.content).toBe(body);
|
|
113
|
+
expect(response.isTimedOut).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('resolves status 300', async () => {
|
|
117
|
+
const reason = 'Multiple Choices';
|
|
118
|
+
|
|
119
|
+
nock(testQueryBaseUrl, { reqheaders: headers })
|
|
120
|
+
.post('/foo')
|
|
121
|
+
.query(testQueryHeader)
|
|
122
|
+
.reply(300, reason);
|
|
123
|
+
|
|
124
|
+
const response = await requester.send(requestStub);
|
|
125
|
+
|
|
126
|
+
expect(response.status).toBe(300);
|
|
127
|
+
expect(response.content).toBe(reason);
|
|
128
|
+
expect(response.isTimedOut).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('resolves status 400', async () => {
|
|
132
|
+
const body = getStringifiedBody({
|
|
133
|
+
message: 'Invalid Application-Id or API-Key',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
nock(testQueryBaseUrl, { reqheaders: headers })
|
|
137
|
+
.post('/foo')
|
|
138
|
+
.query(testQueryHeader)
|
|
139
|
+
.reply(400, body);
|
|
140
|
+
|
|
141
|
+
const response = await requester.send(requestStub);
|
|
142
|
+
|
|
143
|
+
expect(response.status).toBe(400);
|
|
144
|
+
expect(response.content).toBe(body);
|
|
145
|
+
expect(response.isTimedOut).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('handles chunked responses inside unicode character boundaries', async () => {
|
|
149
|
+
const data = Buffer.from('äöü');
|
|
150
|
+
|
|
151
|
+
// create a test response stream that is chunked inside a unicode character
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
153
|
+
function* generate() {
|
|
154
|
+
yield data.slice(0, 3);
|
|
155
|
+
yield data.slice(3);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const testStream = Readable.from(generate());
|
|
159
|
+
|
|
160
|
+
nock(testQueryBaseUrl, { reqheaders: headers })
|
|
161
|
+
.post('/foo')
|
|
162
|
+
.query(testQueryHeader)
|
|
163
|
+
.reply(200, testStream);
|
|
164
|
+
|
|
165
|
+
const response = await requester.send(requestStub);
|
|
166
|
+
|
|
167
|
+
expect(response.content).toEqual(data.toString());
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('timeout handling', () => {
|
|
172
|
+
let server: http.Server;
|
|
173
|
+
// setup http server to test timeout
|
|
174
|
+
beforeAll(() => {
|
|
175
|
+
server = createTestServer();
|
|
176
|
+
|
|
177
|
+
server.listen('1111');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
afterAll((done) => {
|
|
181
|
+
server.close(() => done());
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('timeouts with the given 1 seconds connection timeout', async () => {
|
|
185
|
+
const before = Date.now();
|
|
186
|
+
const response = await requester.send({
|
|
187
|
+
...timeoutRequest,
|
|
188
|
+
connectTimeout: 1000,
|
|
189
|
+
url: 'http://www.google.com:81',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
|
|
194
|
+
expect(response.content).toBe('Connection timeout');
|
|
195
|
+
expect(now - before).toBeGreaterThan(999);
|
|
196
|
+
expect(now - before).toBeLessThan(1200);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('connection timeouts with the given 2 seconds connection timeout', async () => {
|
|
200
|
+
const before = Date.now();
|
|
201
|
+
const response = await requester.send({
|
|
202
|
+
...timeoutRequest,
|
|
203
|
+
connectTimeout: 2000,
|
|
204
|
+
url: 'http://www.google.com:81',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
|
|
209
|
+
expect(response.content).toBe('Connection timeout');
|
|
210
|
+
expect(now - before).toBeGreaterThan(1999);
|
|
211
|
+
expect(now - before).toBeLessThan(2200);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("socket timeouts if response don't appears before the timeout with 2 seconds timeout", async () => {
|
|
215
|
+
const before = Date.now();
|
|
216
|
+
|
|
217
|
+
const response = await requester.send({
|
|
218
|
+
...timeoutRequest,
|
|
219
|
+
responseTimeout: 2000,
|
|
220
|
+
url: 'http://localhost:1111',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
|
|
225
|
+
expect(response.content).toBe('Socket timeout');
|
|
226
|
+
expect(now - before).toBeGreaterThan(1999);
|
|
227
|
+
expect(now - before).toBeLessThan(2200);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("socket timeouts if response don't appears before the timeout with 3 seconds timeout", async () => {
|
|
231
|
+
const before = Date.now();
|
|
232
|
+
const response = await requester.send({
|
|
233
|
+
...timeoutRequest,
|
|
234
|
+
responseTimeout: 3000,
|
|
235
|
+
url: 'http://localhost:1111',
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const now = Date.now();
|
|
239
|
+
|
|
240
|
+
expect(response.content).toBe('Socket timeout');
|
|
241
|
+
expect(now - before).toBeGreaterThan(2999);
|
|
242
|
+
expect(now - before).toBeLessThan(3200);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('do not timeouts if response appears before the timeout', async () => {
|
|
246
|
+
const before = Date.now();
|
|
247
|
+
const response = await requester.send({
|
|
248
|
+
...requestStub,
|
|
249
|
+
url: 'http://localhost:1111',
|
|
250
|
+
responseTimeout: 6000,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const now = Date.now();
|
|
254
|
+
|
|
255
|
+
expect(response.isTimedOut).toBe(false);
|
|
256
|
+
expect(response.status).toBe(200);
|
|
257
|
+
expect(response.content).toBe('{"foo": "bar"}');
|
|
258
|
+
expect(now - before).toBeGreaterThan(4999);
|
|
259
|
+
expect(now - before).toBeLessThan(5200);
|
|
260
|
+
}, 10000); // This is a long-running test, default server timeout is set to 5000ms
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('error handling', (): void => {
|
|
264
|
+
it('resolves dns not found', async () => {
|
|
265
|
+
const request: EndRequest = {
|
|
266
|
+
url: 'https://this-dont-exist.algolia.com',
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers,
|
|
269
|
+
data: getStringifiedBody(),
|
|
270
|
+
responseTimeout: 2000,
|
|
271
|
+
connectTimeout: 1000,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const response = await requester.send(request);
|
|
275
|
+
|
|
276
|
+
expect(response.status).toBe(0);
|
|
277
|
+
expect(response.content).toContain('');
|
|
278
|
+
expect(response.isTimedOut).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('resolves general network errors', async () => {
|
|
282
|
+
nock(testQueryBaseUrl, { reqheaders: headers })
|
|
283
|
+
.post('/foo')
|
|
284
|
+
.query(testQueryHeader)
|
|
285
|
+
.replyWithError('This is a general error');
|
|
286
|
+
|
|
287
|
+
const response = await requester.send(requestStub);
|
|
288
|
+
|
|
289
|
+
expect(response.status).toBe(0);
|
|
290
|
+
expect(response.content).toBe('This is a general error');
|
|
291
|
+
expect(response.isTimedOut).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
|
|
5
|
+
import type { EndRequest, Requester, Response } from '@algolia/client-common';
|
|
6
|
+
|
|
7
|
+
export type CreateHttpRequesterOptions = Partial<{
|
|
8
|
+
agent: http.Agent | https.Agent;
|
|
9
|
+
httpAgent: http.Agent;
|
|
10
|
+
httpsAgent: https.Agent;
|
|
11
|
+
/**
|
|
12
|
+
* RequestOptions to be merged with the end request, it will override default options if provided.
|
|
13
|
+
*/
|
|
14
|
+
requesterOptions: https.RequestOptions;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
// Global agents allow us to reuse the TCP protocol with multiple clients
|
|
18
|
+
const agentOptions = { keepAlive: true };
|
|
19
|
+
const defaultHttpAgent = new http.Agent(agentOptions);
|
|
20
|
+
const defaultHttpsAgent = new https.Agent(agentOptions);
|
|
21
|
+
|
|
22
|
+
export function createHttpRequester({
|
|
23
|
+
agent: userGlobalAgent,
|
|
24
|
+
httpAgent: userHttpAgent,
|
|
25
|
+
httpsAgent: userHttpsAgent,
|
|
26
|
+
requesterOptions = {},
|
|
27
|
+
}: CreateHttpRequesterOptions = {}): Requester {
|
|
28
|
+
const httpAgent = userHttpAgent || userGlobalAgent || defaultHttpAgent;
|
|
29
|
+
const httpsAgent = userHttpsAgent || userGlobalAgent || defaultHttpsAgent;
|
|
30
|
+
|
|
31
|
+
function send(request: EndRequest): Promise<Response> {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
let responseTimeout: NodeJS.Timeout | undefined;
|
|
34
|
+
// eslint-disable-next-line prefer-const -- linter thinks this is not reassigned
|
|
35
|
+
let connectTimeout: NodeJS.Timeout | undefined;
|
|
36
|
+
const url = new URL(request.url);
|
|
37
|
+
const path =
|
|
38
|
+
url.search === null ? url.pathname : `${url.pathname}${url.search}`;
|
|
39
|
+
const options: https.RequestOptions = {
|
|
40
|
+
agent: url.protocol === 'https:' ? httpsAgent : httpAgent,
|
|
41
|
+
hostname: url.hostname,
|
|
42
|
+
path,
|
|
43
|
+
method: request.method,
|
|
44
|
+
...requesterOptions,
|
|
45
|
+
headers: {
|
|
46
|
+
...request.headers,
|
|
47
|
+
...requesterOptions.headers,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (url.port && !requesterOptions.port) {
|
|
52
|
+
options.port = url.port;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const req = (url.protocol === 'https:' ? https : http).request(
|
|
56
|
+
options,
|
|
57
|
+
(response) => {
|
|
58
|
+
let contentBuffers: Buffer[] = [];
|
|
59
|
+
|
|
60
|
+
response.on('data', (chunk) => {
|
|
61
|
+
contentBuffers = contentBuffers.concat(chunk);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
response.on('end', () => {
|
|
65
|
+
clearTimeout(connectTimeout as NodeJS.Timeout);
|
|
66
|
+
clearTimeout(responseTimeout as NodeJS.Timeout);
|
|
67
|
+
|
|
68
|
+
resolve({
|
|
69
|
+
status: response.statusCode || 0,
|
|
70
|
+
content: Buffer.concat(contentBuffers).toString(),
|
|
71
|
+
isTimedOut: false,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const createTimeout = (
|
|
78
|
+
timeout: number,
|
|
79
|
+
content: string
|
|
80
|
+
): NodeJS.Timeout => {
|
|
81
|
+
return setTimeout(() => {
|
|
82
|
+
req.destroy();
|
|
83
|
+
|
|
84
|
+
resolve({
|
|
85
|
+
status: 0,
|
|
86
|
+
content,
|
|
87
|
+
isTimedOut: true,
|
|
88
|
+
});
|
|
89
|
+
}, timeout);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
connectTimeout = createTimeout(
|
|
93
|
+
request.connectTimeout,
|
|
94
|
+
'Connection timeout'
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
req.on('error', (error) => {
|
|
98
|
+
clearTimeout(connectTimeout as NodeJS.Timeout);
|
|
99
|
+
clearTimeout(responseTimeout!);
|
|
100
|
+
resolve({ status: 0, content: error.message, isTimedOut: false });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
req.once('response', () => {
|
|
104
|
+
clearTimeout(connectTimeout as NodeJS.Timeout);
|
|
105
|
+
responseTimeout = createTimeout(
|
|
106
|
+
request.responseTimeout,
|
|
107
|
+
'Socket timeout'
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (request.data !== undefined) {
|
|
112
|
+
req.write(request.data);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
req.end();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { send };
|
|
120
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { URL } from 'url';
|
|
2
|
+
|
|
3
|
+
import { createEchoRequester } from '@algolia/client-common';
|
|
4
|
+
import type { Requester } from '@algolia/client-common';
|
|
5
|
+
|
|
6
|
+
export function echoRequester(status: number = 200): Requester {
|
|
7
|
+
return createEchoRequester({ getURL: (url: string) => new URL(url), status });
|
|
8
|
+
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { Destroyable } from '@algolia/requester-common';
|
|
3
|
-
import * as http from 'http';
|
|
4
|
-
import * as https from 'https';
|
|
5
|
-
import { Requester } from '@algolia/requester-common';
|
|
6
|
-
|
|
7
|
-
export declare function createNodeHttpRequester({ agent: userGlobalAgent, httpAgent: userHttpAgent, httpsAgent: userHttpsAgent, requesterOptions, }?: NodeHttpRequesterOptions): Requester & Destroyable;
|
|
8
|
-
|
|
9
|
-
export declare type NodeHttpRequesterOptions = {
|
|
10
|
-
agent?: https.Agent | http.Agent;
|
|
11
|
-
httpAgent?: http.Agent;
|
|
12
|
-
httpsAgent?: https.Agent;
|
|
13
|
-
requesterOptions?: https.RequestOptions;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export { }
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import * as http from 'http';
|
|
2
|
-
import { Agent } from 'http';
|
|
3
|
-
import * as https from 'https';
|
|
4
|
-
import { Agent as Agent$1 } from 'https';
|
|
5
|
-
import { parse } from 'url';
|
|
6
|
-
|
|
7
|
-
/* eslint functional/prefer-readonly-type: 0 */
|
|
8
|
-
const agentOptions = { keepAlive: true };
|
|
9
|
-
const defaultHttpAgent = new Agent(agentOptions);
|
|
10
|
-
const defaultHttpsAgent = new Agent$1(agentOptions);
|
|
11
|
-
function createNodeHttpRequester({ agent: userGlobalAgent, httpAgent: userHttpAgent, httpsAgent: userHttpsAgent, requesterOptions = {}, } = {}) {
|
|
12
|
-
const httpAgent = userHttpAgent || userGlobalAgent || defaultHttpAgent;
|
|
13
|
-
const httpsAgent = userHttpsAgent || userGlobalAgent || defaultHttpsAgent;
|
|
14
|
-
return {
|
|
15
|
-
send(request) {
|
|
16
|
-
return new Promise(resolve => {
|
|
17
|
-
const url = parse(request.url);
|
|
18
|
-
const path = url.query === null ? url.pathname : `${url.pathname}?${url.query}`;
|
|
19
|
-
const options = {
|
|
20
|
-
...requesterOptions,
|
|
21
|
-
agent: url.protocol === 'https:' ? httpsAgent : httpAgent,
|
|
22
|
-
hostname: url.hostname,
|
|
23
|
-
path,
|
|
24
|
-
method: request.method,
|
|
25
|
-
headers: {
|
|
26
|
-
...(requesterOptions && requesterOptions.headers ? requesterOptions.headers : {}),
|
|
27
|
-
...request.headers,
|
|
28
|
-
},
|
|
29
|
-
...(url.port !== undefined ? { port: url.port || '' } : {}),
|
|
30
|
-
};
|
|
31
|
-
const req = (url.protocol === 'https:' ? https : http).request(options, response => {
|
|
32
|
-
// eslint-disable-next-line functional/no-let
|
|
33
|
-
let contentBuffers = [];
|
|
34
|
-
response.on('data', chunk => {
|
|
35
|
-
contentBuffers = contentBuffers.concat(chunk);
|
|
36
|
-
});
|
|
37
|
-
response.on('end', () => {
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
39
|
-
clearTimeout(connectTimeout);
|
|
40
|
-
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
41
|
-
clearTimeout(responseTimeout);
|
|
42
|
-
resolve({
|
|
43
|
-
status: response.statusCode || 0,
|
|
44
|
-
content: Buffer.concat(contentBuffers).toString(),
|
|
45
|
-
isTimedOut: false,
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
const createTimeout = (timeout, content) => {
|
|
50
|
-
return setTimeout(() => {
|
|
51
|
-
req.abort();
|
|
52
|
-
resolve({
|
|
53
|
-
status: 0,
|
|
54
|
-
content,
|
|
55
|
-
isTimedOut: true,
|
|
56
|
-
});
|
|
57
|
-
}, timeout * 1000);
|
|
58
|
-
};
|
|
59
|
-
const connectTimeout = createTimeout(request.connectTimeout, 'Connection timeout');
|
|
60
|
-
// eslint-disable-next-line functional/no-let
|
|
61
|
-
let responseTimeout;
|
|
62
|
-
req.on('error', error => {
|
|
63
|
-
clearTimeout(connectTimeout);
|
|
64
|
-
clearTimeout(responseTimeout);
|
|
65
|
-
resolve({ status: 0, content: error.message, isTimedOut: false });
|
|
66
|
-
});
|
|
67
|
-
req.once('response', () => {
|
|
68
|
-
clearTimeout(connectTimeout);
|
|
69
|
-
responseTimeout = createTimeout(request.responseTimeout, 'Socket timeout');
|
|
70
|
-
});
|
|
71
|
-
if (request.data !== undefined) {
|
|
72
|
-
req.write(request.data);
|
|
73
|
-
}
|
|
74
|
-
req.end();
|
|
75
|
-
});
|
|
76
|
-
},
|
|
77
|
-
destroy() {
|
|
78
|
-
httpAgent.destroy();
|
|
79
|
-
httpsAgent.destroy();
|
|
80
|
-
return Promise.resolve();
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export { createNodeHttpRequester };
|
package/index.js
DELETED