rekwest 4.4.5 → 4.5.0
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/README.md +12 -12
- package/dist/cookies.js +41 -3
- package/dist/defaults.js +2 -0
- package/dist/index.js +2 -1
- package/dist/postflight.js +8 -4
- package/dist/preflight.js +9 -5
- package/dist/transfer.js +105 -0
- package/dist/transform.js +77 -0
- package/dist/utils.js +8 -163
- package/package.json +3 -3
- package/src/cookies.mjs +53 -7
- package/src/defaults.mjs +2 -0
- package/src/index.mjs +1 -1
- package/src/postflight.mjs +9 -4
- package/src/preflight.mjs +10 -5
- package/src/transfer.mjs +114 -0
- package/src/transform.mjs +75 -0
- package/src/utils.mjs +9 -174
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
The robust request library that humanity deserves 🌐
|
|
2
2
|
---
|
|
3
3
|
This package provides highly likely functional and **easy-to-use** abstraction atop of
|
|
4
|
-
native [http(s).request](https://nodejs.org/api/https.html#
|
|
5
|
-
and [http2.request](https://nodejs.org/api/http2.html#
|
|
4
|
+
native [http(s).request](https://nodejs.org/api/https.html#httpsrequesturl-options-callback)
|
|
5
|
+
and [http2.request](https://nodejs.org/api/http2.html#clienthttp2sessionrequestheaders-options).
|
|
6
6
|
|
|
7
7
|
## Abstract
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ and [http2.request](https://nodejs.org/api/http2.html#http2_clienthttp2session_r
|
|
|
10
10
|
* Cool-beans 🫐 config options (with defaults)
|
|
11
11
|
* Automatic HTTP/2 support (ALPN negotiation)
|
|
12
12
|
* Automatic or opt-in body parse (with non-UTF-8 charset decoding)
|
|
13
|
-
* Automatic and simplistic `Cookies` treatment (with built-in jar)
|
|
13
|
+
* Automatic and simplistic `Cookies` treatment (with built-in **jar** & **ttl**)
|
|
14
14
|
* Automatic decompression (with opt-in body compression)
|
|
15
15
|
* Built-in streamable `File` & `FormData` interfaces
|
|
16
16
|
* Support redirects & retries with fine-grained tune-ups
|
|
@@ -115,17 +115,17 @@ console.log(res.body);
|
|
|
115
115
|
|
|
116
116
|
* `url` **{string | URL}** The URL to send the request to
|
|
117
117
|
* `options` **{Object}**
|
|
118
|
-
Extends [
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
and [tls.ConnectionOptions](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback)
|
|
118
|
+
Extends [http(s).RequestOptions](https://nodejs.org/api/https.html#httpsrequesturl-options-callback) along with
|
|
119
|
+
extra [http2.ClientSessionOptions](https://nodejs.org/api/http2.html#http2connectauthority-options-listener)
|
|
120
|
+
& [http2.ClientSessionRequestOptions](https://nodejs.org/api/http2.html#clienthttp2sessionrequestheaders-options)
|
|
121
|
+
and [tls.ConnectionOptions](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)
|
|
123
122
|
for HTTP/2 attunes
|
|
124
123
|
* `baseURL` **{string | URL}** The base URL to use in cases where `url` is a relative URL
|
|
125
124
|
* `body` **{string | Array | ArrayBuffer | ArrayBufferView | AsyncIterator | Blob | Buffer | DataView | File |
|
|
126
125
|
FormData | Iterator | Object | Readable | SharedArrayBuffer | URLSearchParams}** The body to send with the request
|
|
127
126
|
* `cookies` **{boolean | Array<[k, v]> | Cookies | Object | URLSearchParams}** `Default: true` The cookies to add to
|
|
128
127
|
the request
|
|
128
|
+
* `cookiesTTL` **{boolean}** `Default: false` Controls enablement of TTL for the cookies cache
|
|
129
129
|
* `credentials` **{include | omit | same-origin}** `Default: same-origin` Controls credentials in case of cross-origin
|
|
130
130
|
redirects
|
|
131
131
|
* `digest` **{boolean}** `Default: true` Controls whether to read the response stream or simply add a mixin
|
|
@@ -142,13 +142,13 @@ console.log(res.body);
|
|
|
142
142
|
* `interval` **{number}** `Default: 1e3` The initial retry interval
|
|
143
143
|
* `retryAfter` **{boolean}** `Default: true` Controls `retry-after` header receptiveness
|
|
144
144
|
* `statusCodes` **{number[]}** `Default: [429, 503]` The list of status codes to retry on
|
|
145
|
+
* `stripTrailingSlash` **{boolean}** `Default: false` Controls whether to strip trailing slash at the end of the URL
|
|
145
146
|
* `thenable` **{boolean}** `Default: false` Controls the promise resolutions
|
|
146
147
|
* `timeout` **{number}** `Default: 3e5` The number of milliseconds a request can take before termination
|
|
147
|
-
* `trimTrailingSlashes` **{boolean}** `Default: false` Controls whether to trim trailing slashes
|
|
148
|
-
proceed with the request
|
|
148
|
+
* `trimTrailingSlashes` **{boolean}** `Default: false` Controls whether to trim trailing slashes within the URL
|
|
149
149
|
* **Returns:** Promise that resolves to
|
|
150
|
-
extended [http.IncomingMessage](https://nodejs.org/api/http.html#
|
|
151
|
-
or [http2.ClientHttp2Stream](https://nodejs.org/api/http2.html#
|
|
150
|
+
extended [http.IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage)
|
|
151
|
+
or [http2.ClientHttp2Stream](https://nodejs.org/api/http2.html#class-clienthttp2stream) which is respectively
|
|
152
152
|
readable and duplex streams
|
|
153
153
|
* if `degist: true` & `parse: true`
|
|
154
154
|
* `body` **{string | Array | Buffer | Object}** The body based on its content type
|
package/dist/cookies.js
CHANGED
|
@@ -5,14 +5,52 @@ exports.Cookies = void 0;
|
|
|
5
5
|
var _utils = require("./utils");
|
|
6
6
|
class Cookies extends URLSearchParams {
|
|
7
7
|
static jar = new Map();
|
|
8
|
+
#chronometry = new Map();
|
|
8
9
|
get [Symbol.toStringTag]() {
|
|
9
10
|
return this.constructor.name;
|
|
10
11
|
}
|
|
11
|
-
constructor(input
|
|
12
|
+
constructor(input, {
|
|
13
|
+
cookiesTTL
|
|
14
|
+
} = {
|
|
15
|
+
cookiesTTL: false
|
|
16
|
+
}) {
|
|
12
17
|
if (Array.isArray(input) && input.every(it => !Array.isArray(it))) {
|
|
13
|
-
input = input.
|
|
18
|
+
input = input.map(it => {
|
|
19
|
+
if (!cookiesTTL) {
|
|
20
|
+
return [it.split(';').at(0).trim()];
|
|
21
|
+
}
|
|
22
|
+
const [cookie, ...attrs] = it.split(';').map(it => it.trim());
|
|
23
|
+
const ttl = attrs.reduce((acc, val) => {
|
|
24
|
+
if (/(?:Expires|Max-Age)=/i.test(val)) {
|
|
25
|
+
const [key, value] = val.toLowerCase().split('=');
|
|
26
|
+
acc[(0, _utils.toCamelCase)(key)] = !Number.isNaN(Number(value)) ? value * 1e3 : Date.parse(value) - Date.now();
|
|
27
|
+
}
|
|
28
|
+
return acc;
|
|
29
|
+
}, {});
|
|
30
|
+
return [cookie.replace(/\u0022/g, ''), Object.keys(ttl).length ? ttl : null];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
super(Array.isArray(input) ? input.map(it => it.at(0)).join('&') : input);
|
|
34
|
+
if (Array.isArray(input) && cookiesTTL) {
|
|
35
|
+
input.filter(it => it.at(1)).forEach(([cookie, ttl]) => {
|
|
36
|
+
cookie = cookie.split('=').at(0);
|
|
37
|
+
if (this.#chronometry.has(cookie)) {
|
|
38
|
+
clearTimeout(this.#chronometry.get(cookie));
|
|
39
|
+
this.#chronometry.delete(cookie);
|
|
40
|
+
}
|
|
41
|
+
const {
|
|
42
|
+
expires,
|
|
43
|
+
maxAge
|
|
44
|
+
} = ttl;
|
|
45
|
+
[maxAge, expires].filter(it => Number.isInteger(it)).some(ms => {
|
|
46
|
+
const tid = setTimeout(() => {
|
|
47
|
+
this.#chronometry.delete(cookie);
|
|
48
|
+
this.delete(cookie);
|
|
49
|
+
}, Math.max(ms, 0));
|
|
50
|
+
return this.#chronometry.set(cookie, tid);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
14
53
|
}
|
|
15
|
-
super(input);
|
|
16
54
|
}
|
|
17
55
|
toString() {
|
|
18
56
|
(0, _utils.brandCheck)(this, Cookies);
|
package/dist/defaults.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
HTTP_STATUS_TOO_MANY_REQUESTS
|
|
13
13
|
} = _nodeHttp.default.constants;
|
|
14
14
|
const stash = {
|
|
15
|
+
cookiesTTL: false,
|
|
15
16
|
credentials: _constants.requestCredentials.sameOrigin,
|
|
16
17
|
digest: true,
|
|
17
18
|
follow: 20,
|
|
@@ -33,6 +34,7 @@ const stash = {
|
|
|
33
34
|
retryAfter: true,
|
|
34
35
|
statusCodes: [HTTP_STATUS_TOO_MANY_REQUESTS, HTTP_STATUS_SERVICE_UNAVAILABLE]
|
|
35
36
|
},
|
|
37
|
+
stripTrailingSlash: false,
|
|
36
38
|
thenable: false,
|
|
37
39
|
timeout: 3e5,
|
|
38
40
|
trimTrailingSlashes: false
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ var _defaults = _interopRequireDefault(require("./defaults"));
|
|
|
23
23
|
var _mediatypes = _interopRequireWildcard(require("./mediatypes"));
|
|
24
24
|
exports.mediatypes = _mediatypes;
|
|
25
25
|
var _preflight = require("./preflight");
|
|
26
|
+
var _transfer = require("./transfer");
|
|
26
27
|
var _utils = require("./utils");
|
|
27
28
|
Object.keys(_utils).forEach(function (key) {
|
|
28
29
|
if (key === "default" || key === "__esModule") return;
|
|
@@ -86,7 +87,7 @@ const {
|
|
|
86
87
|
HTTP2_HEADER_CONTENT_TYPE
|
|
87
88
|
} = _nodeHttp2.default.constants;
|
|
88
89
|
function rekwest(url, options) {
|
|
89
|
-
return (0,
|
|
90
|
+
return (0, _transfer.transfer)((0, _validation.validation)((0, _utils.normalize)(url, options)), rekwest);
|
|
90
91
|
}
|
|
91
92
|
Reflect.defineProperty(rekwest, 'defaults', {
|
|
92
93
|
enumerable: true,
|
package/dist/postflight.js
CHANGED
|
@@ -46,11 +46,15 @@ const postflight = (req, res, options, {
|
|
|
46
46
|
(0, _utils.admix)(res, headers, options);
|
|
47
47
|
if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
|
|
48
48
|
if (_cookies.Cookies.jar.has(url.origin)) {
|
|
49
|
-
new _cookies.Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
const cookie = new _cookies.Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options);
|
|
50
|
+
_cookies.Cookies.jar.get(url.origin).forEach((val, key) => {
|
|
51
|
+
if (!cookie.has(key)) {
|
|
52
|
+
cookie.set(key, val);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
_cookies.Cookies.jar.set(url.origin, cookie);
|
|
52
56
|
} else {
|
|
53
|
-
_cookies.Cookies.jar.set(url.origin, new _cookies.Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]));
|
|
57
|
+
_cookies.Cookies.jar.set(url.origin, new _cookies.Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options));
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
Reflect.defineProperty(res, 'cookies', {
|
package/dist/preflight.js
CHANGED
|
@@ -31,14 +31,18 @@ const preflight = options => {
|
|
|
31
31
|
options.endStream = [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(method);
|
|
32
32
|
}
|
|
33
33
|
if (cookies !== false) {
|
|
34
|
-
let cookie = _cookies.Cookies.jar.
|
|
34
|
+
let cookie = _cookies.Cookies.jar.has(url.origin);
|
|
35
35
|
if (cookies === Object(cookies) && [_constants.requestCredentials.include, _constants.requestCredentials.sameOrigin].includes(credentials)) {
|
|
36
36
|
if (cookie) {
|
|
37
|
-
new _cookies.Cookies(cookies
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
cookie = new _cookies.Cookies(cookies, options);
|
|
38
|
+
_cookies.Cookies.jar.get(url.origin).forEach((val, key) => {
|
|
39
|
+
if (!cookie.has(key)) {
|
|
40
|
+
cookie.set(key, val);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
_cookies.Cookies.jar.set(url.origin, cookie);
|
|
40
44
|
} else {
|
|
41
|
-
cookie = new _cookies.Cookies(cookies);
|
|
45
|
+
cookie = new _cookies.Cookies(cookies, options);
|
|
42
46
|
_cookies.Cookies.jar.set(url.origin, cookie);
|
|
43
47
|
}
|
|
44
48
|
}
|
package/dist/transfer.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.transfer = void 0;
|
|
5
|
+
var _nodeHttp = _interopRequireDefault(require("node:http"));
|
|
6
|
+
var _nodeHttp2 = _interopRequireDefault(require("node:http2"));
|
|
7
|
+
var _nodeHttps = _interopRequireDefault(require("node:https"));
|
|
8
|
+
var _promises = require("node:timers/promises");
|
|
9
|
+
var _ackn = require("./ackn");
|
|
10
|
+
var _errors = require("./errors");
|
|
11
|
+
var _postflight = require("./postflight");
|
|
12
|
+
var _preflight = require("./preflight");
|
|
13
|
+
var _transform = require("./transform");
|
|
14
|
+
var _utils = require("./utils");
|
|
15
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
16
|
+
const {
|
|
17
|
+
HTTP2_HEADER_RETRY_AFTER
|
|
18
|
+
} = _nodeHttp2.default.constants;
|
|
19
|
+
const transfer = async (options, overact) => {
|
|
20
|
+
const {
|
|
21
|
+
digest,
|
|
22
|
+
redirected,
|
|
23
|
+
thenable,
|
|
24
|
+
url
|
|
25
|
+
} = options;
|
|
26
|
+
if (options.follow === 0) {
|
|
27
|
+
throw new _errors.RequestError(`Maximum redirect reached at: ${url.href}`);
|
|
28
|
+
}
|
|
29
|
+
if (url.protocol === 'https:') {
|
|
30
|
+
options = !options.h2 ? await (0, _ackn.ackn)(options) : {
|
|
31
|
+
...options,
|
|
32
|
+
createConnection: null,
|
|
33
|
+
protocol: url.protocol
|
|
34
|
+
};
|
|
35
|
+
} else if (Reflect.has(options, 'alpnProtocol')) {
|
|
36
|
+
['alpnProtocol', 'createConnection', 'h2', 'protocol'].forEach(it => Reflect.deleteProperty(options, it));
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
options = await (0, _transform.transform)((0, _preflight.preflight)(options));
|
|
40
|
+
} catch (ex) {
|
|
41
|
+
options.createConnection?.().destroy();
|
|
42
|
+
throw ex;
|
|
43
|
+
}
|
|
44
|
+
const promise = new Promise((resolve, reject) => {
|
|
45
|
+
let client, req;
|
|
46
|
+
if (options.h2) {
|
|
47
|
+
client = _nodeHttp2.default.connect(url.origin, options);
|
|
48
|
+
req = client.request(options.headers, options);
|
|
49
|
+
} else {
|
|
50
|
+
const {
|
|
51
|
+
request
|
|
52
|
+
} = url.protocol === 'http:' ? _nodeHttp.default : _nodeHttps.default;
|
|
53
|
+
req = request(url, options);
|
|
54
|
+
}
|
|
55
|
+
(0, _utils.affix)(client, req, options);
|
|
56
|
+
req.once('error', reject);
|
|
57
|
+
req.once('frameError', reject);
|
|
58
|
+
req.once('goaway', reject);
|
|
59
|
+
req.once('response', res => (0, _postflight.postflight)(req, res, options, {
|
|
60
|
+
reject,
|
|
61
|
+
resolve
|
|
62
|
+
}));
|
|
63
|
+
(0, _utils.dispatch)(options, req);
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
const res = await promise;
|
|
67
|
+
if (digest && !redirected) {
|
|
68
|
+
res.body = await res.body();
|
|
69
|
+
}
|
|
70
|
+
return res;
|
|
71
|
+
} catch (ex) {
|
|
72
|
+
const {
|
|
73
|
+
maxRetryAfter,
|
|
74
|
+
retry
|
|
75
|
+
} = options;
|
|
76
|
+
if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
|
|
77
|
+
let {
|
|
78
|
+
interval
|
|
79
|
+
} = retry;
|
|
80
|
+
if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
|
|
81
|
+
interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
|
|
82
|
+
interval = Number(interval) * 1000 || new Date(interval) - Date.now();
|
|
83
|
+
if (interval > maxRetryAfter) {
|
|
84
|
+
throw (0, _utils.maxRetryAfterError)(interval, {
|
|
85
|
+
cause: ex
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
interval = new Function('interval', `return Math.ceil(${retry.backoffStrategy});`)(interval);
|
|
90
|
+
}
|
|
91
|
+
retry.attempts--;
|
|
92
|
+
retry.interval = interval;
|
|
93
|
+
return (0, _promises.setTimeout)(interval).then(() => overact(url, options));
|
|
94
|
+
}
|
|
95
|
+
if (digest && !redirected && ex.body) {
|
|
96
|
+
ex.body = await ex.body();
|
|
97
|
+
}
|
|
98
|
+
if (!thenable) {
|
|
99
|
+
throw ex;
|
|
100
|
+
} else {
|
|
101
|
+
return ex;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
exports.transfer = transfer;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.transform = void 0;
|
|
5
|
+
var _nodeHttp = _interopRequireDefault(require("node:http2"));
|
|
6
|
+
var _nodeStream = require("node:stream");
|
|
7
|
+
var _consumers = require("node:stream/consumers");
|
|
8
|
+
var _nodeUtil = require("node:util");
|
|
9
|
+
var _file = require("./file");
|
|
10
|
+
var _formdata = require("./formdata");
|
|
11
|
+
var _mediatypes = require("./mediatypes");
|
|
12
|
+
var _utils = require("./utils");
|
|
13
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
14
|
+
const {
|
|
15
|
+
HTTP2_HEADER_CONTENT_ENCODING,
|
|
16
|
+
HTTP2_HEADER_CONTENT_LENGTH,
|
|
17
|
+
HTTP2_HEADER_CONTENT_TYPE
|
|
18
|
+
} = _nodeHttp.default.constants;
|
|
19
|
+
const transform = async options => {
|
|
20
|
+
let {
|
|
21
|
+
body,
|
|
22
|
+
headers
|
|
23
|
+
} = options;
|
|
24
|
+
if (!body) {
|
|
25
|
+
return options;
|
|
26
|
+
}
|
|
27
|
+
if (_file.File.alike(body)) {
|
|
28
|
+
headers = {
|
|
29
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: body.size,
|
|
30
|
+
[HTTP2_HEADER_CONTENT_TYPE]: body.type || _mediatypes.APPLICATION_OCTET_STREAM
|
|
31
|
+
};
|
|
32
|
+
body = body.stream();
|
|
33
|
+
} else if (_formdata.FormData.alike(body)) {
|
|
34
|
+
body = _formdata.FormData.actuate(body);
|
|
35
|
+
headers = {
|
|
36
|
+
[HTTP2_HEADER_CONTENT_TYPE]: body.contentType
|
|
37
|
+
};
|
|
38
|
+
} else if (!Buffer.isBuffer(body)) {
|
|
39
|
+
if (_nodeUtil.types.isAnyArrayBuffer(body)) {
|
|
40
|
+
body = Buffer.from(body);
|
|
41
|
+
} else if (_nodeUtil.types.isArrayBufferView(body)) {
|
|
42
|
+
body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
43
|
+
} else if (body === Object(body) && !Reflect.has(body, Symbol.asyncIterator)) {
|
|
44
|
+
if (body.constructor === URLSearchParams) {
|
|
45
|
+
headers = {
|
|
46
|
+
[HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_FORM_URLENCODED
|
|
47
|
+
};
|
|
48
|
+
body = body.toString();
|
|
49
|
+
} else if (!(!Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
|
|
50
|
+
headers = {
|
|
51
|
+
[HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_JSON
|
|
52
|
+
};
|
|
53
|
+
body = JSON.stringify(body);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const encodings = options.headers[HTTP2_HEADER_CONTENT_ENCODING];
|
|
58
|
+
if (body === Object(body) && (Reflect.has(body, Symbol.asyncIterator) || !Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
|
|
59
|
+
body = encodings ? (0, _utils.compress)(_nodeStream.Readable.from(body), encodings) : _nodeStream.Readable.from(body);
|
|
60
|
+
} else if (encodings) {
|
|
61
|
+
body = await (0, _consumers.buffer)((0, _utils.compress)(_nodeStream.Readable.from(body), encodings));
|
|
62
|
+
}
|
|
63
|
+
Object.assign(options.headers, {
|
|
64
|
+
...headers,
|
|
65
|
+
...(!body[Symbol.asyncIterator] && {
|
|
66
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body)
|
|
67
|
+
}),
|
|
68
|
+
...(options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
|
|
69
|
+
[HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE]
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
...options,
|
|
74
|
+
body
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
exports.transform = transform;
|
package/dist/utils.js
CHANGED
|
@@ -3,31 +3,17 @@
|
|
|
3
3
|
exports.__esModule = true;
|
|
4
4
|
exports.sameOrigin = exports.normalize = exports.merge = exports.maxRetryAfterError = exports.maxRetryAfter = exports.dispatch = exports.decompress = exports.compress = exports.brandCheck = exports.affix = exports.admix = void 0;
|
|
5
5
|
exports.tap = tap;
|
|
6
|
-
exports.unwind = exports.
|
|
7
|
-
var _nodeHttp = _interopRequireDefault(require("node:
|
|
8
|
-
var _nodeHttp2 = _interopRequireDefault(require("node:http2"));
|
|
9
|
-
var _nodeHttps = _interopRequireDefault(require("node:https"));
|
|
6
|
+
exports.unwind = exports.toCamelCase = void 0;
|
|
7
|
+
var _nodeHttp = _interopRequireDefault(require("node:http2"));
|
|
10
8
|
var _nodeStream = require("node:stream");
|
|
11
|
-
var _consumers = require("node:stream/consumers");
|
|
12
|
-
var _promises = require("node:timers/promises");
|
|
13
|
-
var _nodeUtil = require("node:util");
|
|
14
9
|
var _nodeZlib = _interopRequireDefault(require("node:zlib"));
|
|
15
|
-
var _ackn = require("./ackn");
|
|
16
10
|
var _defaults = _interopRequireDefault(require("./defaults"));
|
|
17
11
|
var _errors = require("./errors");
|
|
18
|
-
var _file = require("./file");
|
|
19
|
-
var _formdata = require("./formdata");
|
|
20
|
-
var _mediatypes = require("./mediatypes");
|
|
21
|
-
var _postflight = require("./postflight");
|
|
22
|
-
var _preflight = require("./preflight");
|
|
23
12
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
24
13
|
const {
|
|
25
|
-
HTTP2_HEADER_CONTENT_ENCODING,
|
|
26
|
-
HTTP2_HEADER_CONTENT_LENGTH,
|
|
27
|
-
HTTP2_HEADER_CONTENT_TYPE,
|
|
28
14
|
HTTP2_HEADER_RETRY_AFTER,
|
|
29
15
|
HTTP2_HEADER_STATUS
|
|
30
|
-
} =
|
|
16
|
+
} = _nodeHttp.default.constants;
|
|
31
17
|
const admix = (res, headers, options) => {
|
|
32
18
|
const {
|
|
33
19
|
h2
|
|
@@ -154,6 +140,9 @@ const normalize = (url, options = {}) => {
|
|
|
154
140
|
if (options.trimTrailingSlashes) {
|
|
155
141
|
url = `${url}`.replace(/(?<!:)\/+/g, '/');
|
|
156
142
|
}
|
|
143
|
+
if (options.stripTrailingSlash) {
|
|
144
|
+
url = `${url}`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
|
|
145
|
+
}
|
|
157
146
|
url = new URL(url, options.baseURL);
|
|
158
147
|
return Object.assign(options, {
|
|
159
148
|
url
|
|
@@ -171,151 +160,7 @@ async function* tap(value) {
|
|
|
171
160
|
yield await value.arrayBuffer();
|
|
172
161
|
}
|
|
173
162
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
digest,
|
|
177
|
-
redirected,
|
|
178
|
-
thenable,
|
|
179
|
-
url
|
|
180
|
-
} = options;
|
|
181
|
-
if (options.follow === 0) {
|
|
182
|
-
throw new _errors.RequestError(`Maximum redirect reached at: ${url.href}`);
|
|
183
|
-
}
|
|
184
|
-
if (url.protocol === 'https:') {
|
|
185
|
-
options = !options.h2 ? await (0, _ackn.ackn)(options) : {
|
|
186
|
-
...options,
|
|
187
|
-
createConnection: null,
|
|
188
|
-
protocol: url.protocol
|
|
189
|
-
};
|
|
190
|
-
} else if (Reflect.has(options, 'alpnProtocol')) {
|
|
191
|
-
['alpnProtocol', 'createConnection', 'h2', 'protocol'].forEach(it => Reflect.deleteProperty(options, it));
|
|
192
|
-
}
|
|
193
|
-
try {
|
|
194
|
-
options = await transform((0, _preflight.preflight)(options));
|
|
195
|
-
} catch (ex) {
|
|
196
|
-
options.createConnection?.().destroy();
|
|
197
|
-
throw ex;
|
|
198
|
-
}
|
|
199
|
-
const promise = new Promise((resolve, reject) => {
|
|
200
|
-
let client, req;
|
|
201
|
-
if (options.h2) {
|
|
202
|
-
client = _nodeHttp2.default.connect(url.origin, options);
|
|
203
|
-
req = client.request(options.headers, options);
|
|
204
|
-
} else {
|
|
205
|
-
const {
|
|
206
|
-
request
|
|
207
|
-
} = url.protocol === 'http:' ? _nodeHttp.default : _nodeHttps.default;
|
|
208
|
-
req = request(url, options);
|
|
209
|
-
}
|
|
210
|
-
affix(client, req, options);
|
|
211
|
-
req.once('error', reject);
|
|
212
|
-
req.once('frameError', reject);
|
|
213
|
-
req.once('goaway', reject);
|
|
214
|
-
req.once('response', res => (0, _postflight.postflight)(req, res, options, {
|
|
215
|
-
reject,
|
|
216
|
-
resolve
|
|
217
|
-
}));
|
|
218
|
-
dispatch(options, req);
|
|
219
|
-
});
|
|
220
|
-
try {
|
|
221
|
-
const res = await promise;
|
|
222
|
-
if (digest && !redirected) {
|
|
223
|
-
res.body = await res.body();
|
|
224
|
-
}
|
|
225
|
-
return res;
|
|
226
|
-
} catch (ex) {
|
|
227
|
-
const {
|
|
228
|
-
maxRetryAfter,
|
|
229
|
-
retry
|
|
230
|
-
} = options;
|
|
231
|
-
if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
|
|
232
|
-
let {
|
|
233
|
-
interval
|
|
234
|
-
} = retry;
|
|
235
|
-
if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
|
|
236
|
-
interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
|
|
237
|
-
interval = Number(interval) * 1000 || new Date(interval) - Date.now();
|
|
238
|
-
if (interval > maxRetryAfter) {
|
|
239
|
-
throw maxRetryAfterError(interval, {
|
|
240
|
-
cause: ex
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
interval = new Function('interval', `return Math.ceil(${retry.backoffStrategy});`)(interval);
|
|
245
|
-
}
|
|
246
|
-
retry.attempts--;
|
|
247
|
-
retry.interval = interval;
|
|
248
|
-
return (0, _promises.setTimeout)(interval).then(() => overact(url, options));
|
|
249
|
-
}
|
|
250
|
-
if (digest && !redirected && ex.body) {
|
|
251
|
-
ex.body = await ex.body();
|
|
252
|
-
}
|
|
253
|
-
if (!thenable) {
|
|
254
|
-
throw ex;
|
|
255
|
-
} else {
|
|
256
|
-
return ex;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
exports.transfer = transfer;
|
|
261
|
-
const transform = async options => {
|
|
262
|
-
let {
|
|
263
|
-
body,
|
|
264
|
-
headers
|
|
265
|
-
} = options;
|
|
266
|
-
if (!body) {
|
|
267
|
-
return options;
|
|
268
|
-
}
|
|
269
|
-
if (_file.File.alike(body)) {
|
|
270
|
-
headers = {
|
|
271
|
-
[HTTP2_HEADER_CONTENT_LENGTH]: body.size,
|
|
272
|
-
[HTTP2_HEADER_CONTENT_TYPE]: body.type || _mediatypes.APPLICATION_OCTET_STREAM
|
|
273
|
-
};
|
|
274
|
-
body = body.stream();
|
|
275
|
-
} else if (_formdata.FormData.alike(body)) {
|
|
276
|
-
body = _formdata.FormData.actuate(body);
|
|
277
|
-
headers = {
|
|
278
|
-
[HTTP2_HEADER_CONTENT_TYPE]: body.contentType
|
|
279
|
-
};
|
|
280
|
-
} else if (!Buffer.isBuffer(body)) {
|
|
281
|
-
if (_nodeUtil.types.isAnyArrayBuffer(body)) {
|
|
282
|
-
body = Buffer.from(body);
|
|
283
|
-
} else if (_nodeUtil.types.isArrayBufferView(body)) {
|
|
284
|
-
body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
285
|
-
} else if (body === Object(body) && !Reflect.has(body, Symbol.asyncIterator)) {
|
|
286
|
-
if (body.constructor === URLSearchParams) {
|
|
287
|
-
headers = {
|
|
288
|
-
[HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_FORM_URLENCODED
|
|
289
|
-
};
|
|
290
|
-
body = body.toString();
|
|
291
|
-
} else if (!(!Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
|
|
292
|
-
headers = {
|
|
293
|
-
[HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_JSON
|
|
294
|
-
};
|
|
295
|
-
body = JSON.stringify(body);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
const encodings = options.headers[HTTP2_HEADER_CONTENT_ENCODING];
|
|
300
|
-
if (body === Object(body) && (Reflect.has(body, Symbol.asyncIterator) || !Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
|
|
301
|
-
body = encodings ? compress(_nodeStream.Readable.from(body), encodings) : _nodeStream.Readable.from(body);
|
|
302
|
-
} else if (encodings) {
|
|
303
|
-
body = await (0, _consumers.buffer)(compress(_nodeStream.Readable.from(body), encodings));
|
|
304
|
-
}
|
|
305
|
-
Object.assign(options.headers, {
|
|
306
|
-
...headers,
|
|
307
|
-
...(!body[Symbol.asyncIterator] && {
|
|
308
|
-
[HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body)
|
|
309
|
-
}),
|
|
310
|
-
...(options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
|
|
311
|
-
[HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE]
|
|
312
|
-
})
|
|
313
|
-
});
|
|
314
|
-
return {
|
|
315
|
-
...options,
|
|
316
|
-
body
|
|
317
|
-
};
|
|
318
|
-
};
|
|
319
|
-
exports.transform = transform;
|
|
163
|
+
const toCamelCase = str => str?.toLowerCase().replace(/\p{Punctuation}.|\p{White_Space}./gu, val => val.replace(/\p{Punctuation}+|\p{White_Space}+/gu, '').toUpperCase());
|
|
164
|
+
exports.toCamelCase = toCamelCase;
|
|
320
165
|
const unwind = encodings => encodings.split(',').map(it => it.trim());
|
|
321
166
|
exports.unwind = unwind;
|
package/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@babel/cli": "^7.21.5",
|
|
12
12
|
"@babel/core": "^7.22.1",
|
|
13
13
|
"@babel/eslint-parser": "^7.21.8",
|
|
14
|
-
"@babel/preset-env": "^7.22.
|
|
15
|
-
"c8": "^7.
|
|
14
|
+
"@babel/preset-env": "^7.22.4",
|
|
15
|
+
"c8": "^7.14.0",
|
|
16
16
|
"eslint": "^8.41.0",
|
|
17
17
|
"eslint-config-ultra-refined": "^2.15.0",
|
|
18
18
|
"mocha": "^10.2.0"
|
|
@@ -67,5 +67,5 @@
|
|
|
67
67
|
"test:bail": "mocha --bail",
|
|
68
68
|
"test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
|
|
69
69
|
},
|
|
70
|
-
"version": "4.
|
|
70
|
+
"version": "4.5.0"
|
|
71
71
|
}
|
package/src/cookies.mjs
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
brandCheck,
|
|
3
|
+
toCamelCase,
|
|
4
|
+
} from './utils.mjs';
|
|
2
5
|
|
|
3
6
|
export class Cookies extends URLSearchParams {
|
|
4
7
|
|
|
5
8
|
static jar = new Map();
|
|
6
9
|
|
|
10
|
+
#chronometry = new Map();
|
|
11
|
+
|
|
7
12
|
get [Symbol.toStringTag]() {
|
|
8
13
|
return this.constructor.name;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
constructor(input) {
|
|
16
|
+
constructor(input, { cookiesTTL } = { cookiesTTL: false }) {
|
|
12
17
|
if (Array.isArray(input) && input.every((it) => !Array.isArray(it))) {
|
|
13
|
-
input = input.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
input = input.map((it) => {
|
|
19
|
+
if (!cookiesTTL) {
|
|
20
|
+
return [it.split(';').at(0).trim()];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const [cookie, ...attrs] = it.split(';').map((it) => it.trim());
|
|
24
|
+
const ttl = attrs.reduce((acc, val) => {
|
|
25
|
+
if (/(?:Expires|Max-Age)=/i.test(val)) {
|
|
26
|
+
const [key, value] = val.toLowerCase().split('=');
|
|
27
|
+
|
|
28
|
+
acc[toCamelCase(key)] = !Number.isNaN(Number(value)) ? value * 1e3 : Date.parse(value) - Date.now();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return acc;
|
|
32
|
+
}, {});
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
cookie.replace(/\u0022/g, ''),
|
|
36
|
+
Object.keys(ttl).length ? ttl : null,
|
|
37
|
+
];
|
|
38
|
+
});
|
|
17
39
|
}
|
|
18
40
|
|
|
19
|
-
super(input);
|
|
41
|
+
super(Array.isArray(input) ? input.map((it) => it.at(0)).join('&') : input);
|
|
42
|
+
|
|
43
|
+
if (Array.isArray(input) && cookiesTTL) {
|
|
44
|
+
input.filter((it) => it.at(1)).forEach(([cookie, ttl]) => {
|
|
45
|
+
cookie = cookie.split('=').at(0);
|
|
46
|
+
if (this.#chronometry.has(cookie)) {
|
|
47
|
+
clearTimeout(this.#chronometry.get(cookie));
|
|
48
|
+
this.#chronometry.delete(cookie);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { expires, maxAge } = ttl;
|
|
52
|
+
|
|
53
|
+
[
|
|
54
|
+
maxAge,
|
|
55
|
+
expires,
|
|
56
|
+
].filter((it) => Number.isInteger(it)).some((ms) => {
|
|
57
|
+
const tid = setTimeout(() => {
|
|
58
|
+
this.#chronometry.delete(cookie);
|
|
59
|
+
this.delete(cookie);
|
|
60
|
+
}, Math.max(ms, 0));
|
|
61
|
+
|
|
62
|
+
return this.#chronometry.set(cookie, tid);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
20
66
|
}
|
|
21
67
|
|
|
22
68
|
toString() {
|
package/src/defaults.mjs
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
} = http2.constants;
|
|
13
13
|
|
|
14
14
|
const stash = {
|
|
15
|
+
cookiesTTL: false,
|
|
15
16
|
credentials: requestCredentials.sameOrigin,
|
|
16
17
|
digest: true,
|
|
17
18
|
follow: 20,
|
|
@@ -36,6 +37,7 @@ const stash = {
|
|
|
36
37
|
HTTP_STATUS_SERVICE_UNAVAILABLE,
|
|
37
38
|
],
|
|
38
39
|
},
|
|
40
|
+
stripTrailingSlash: false,
|
|
39
41
|
thenable: false,
|
|
40
42
|
timeout: 3e5,
|
|
41
43
|
trimTrailingSlashes: false,
|
package/src/index.mjs
CHANGED
|
@@ -5,12 +5,12 @@ import { requestRedirect } from './constants.mjs';
|
|
|
5
5
|
import defaults from './defaults.mjs';
|
|
6
6
|
import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
|
|
7
7
|
import { preflight } from './preflight.mjs';
|
|
8
|
+
import { transfer } from './transfer.mjs';
|
|
8
9
|
import {
|
|
9
10
|
admix,
|
|
10
11
|
affix,
|
|
11
12
|
merge,
|
|
12
13
|
normalize,
|
|
13
|
-
transfer,
|
|
14
14
|
} from './utils.mjs';
|
|
15
15
|
import { validation } from './validation.mjs';
|
|
16
16
|
|
package/src/postflight.mjs
CHANGED
|
@@ -44,11 +44,16 @@ export const postflight = (req, res, options, { reject, resolve }) => {
|
|
|
44
44
|
|
|
45
45
|
if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
|
|
46
46
|
if (Cookies.jar.has(url.origin)) {
|
|
47
|
-
new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const cookie = new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options);
|
|
48
|
+
|
|
49
|
+
Cookies.jar.get(url.origin).forEach((val, key) => {
|
|
50
|
+
if (!cookie.has(key)) {
|
|
51
|
+
cookie.set(key, val);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
Cookies.jar.set(url.origin, cookie);
|
|
50
55
|
} else {
|
|
51
|
-
Cookies.jar.set(url.origin, new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]));
|
|
56
|
+
Cookies.jar.set(url.origin, new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options));
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
|
package/src/preflight.mjs
CHANGED
|
@@ -30,18 +30,23 @@ export const preflight = (options) => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (cookies !== false) {
|
|
33
|
-
let cookie = Cookies.jar.
|
|
33
|
+
let cookie = Cookies.jar.has(url.origin);
|
|
34
34
|
|
|
35
35
|
if (cookies === Object(cookies) && [
|
|
36
36
|
requestCredentials.include,
|
|
37
37
|
requestCredentials.sameOrigin,
|
|
38
38
|
].includes(credentials)) {
|
|
39
39
|
if (cookie) {
|
|
40
|
-
new Cookies(cookies
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
cookie = new Cookies(cookies, options);
|
|
41
|
+
|
|
42
|
+
Cookies.jar.get(url.origin).forEach((val, key) => {
|
|
43
|
+
if (!cookie.has(key)) {
|
|
44
|
+
cookie.set(key, val);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
Cookies.jar.set(url.origin, cookie);
|
|
43
48
|
} else {
|
|
44
|
-
cookie = new Cookies(cookies);
|
|
49
|
+
cookie = new Cookies(cookies, options);
|
|
45
50
|
Cookies.jar.set(url.origin, cookie);
|
|
46
51
|
}
|
|
47
52
|
}
|
package/src/transfer.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import http2 from 'node:http2';
|
|
3
|
+
import https from 'node:https';
|
|
4
|
+
import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
|
|
5
|
+
import { ackn } from './ackn.mjs';
|
|
6
|
+
import { RequestError } from './errors.mjs';
|
|
7
|
+
import { postflight } from './postflight.mjs';
|
|
8
|
+
import { preflight } from './preflight.mjs';
|
|
9
|
+
import { transform } from './transform.mjs';
|
|
10
|
+
import {
|
|
11
|
+
affix,
|
|
12
|
+
dispatch,
|
|
13
|
+
maxRetryAfterError,
|
|
14
|
+
} from './utils.mjs';
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
HTTP2_HEADER_RETRY_AFTER,
|
|
18
|
+
} = http2.constants;
|
|
19
|
+
|
|
20
|
+
export const transfer = async (options, overact) => {
|
|
21
|
+
const { digest, redirected, thenable, url } = options;
|
|
22
|
+
|
|
23
|
+
if (options.follow === 0) {
|
|
24
|
+
throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (url.protocol === 'https:') {
|
|
28
|
+
options = !options.h2 ? await ackn(options) : {
|
|
29
|
+
...options,
|
|
30
|
+
createConnection: null,
|
|
31
|
+
protocol: url.protocol,
|
|
32
|
+
};
|
|
33
|
+
} else if (Reflect.has(options, 'alpnProtocol')) {
|
|
34
|
+
[
|
|
35
|
+
'alpnProtocol',
|
|
36
|
+
'createConnection',
|
|
37
|
+
'h2',
|
|
38
|
+
'protocol',
|
|
39
|
+
].forEach((it) => Reflect.deleteProperty(options, it));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
options = await transform(preflight(options));
|
|
44
|
+
} catch (ex) {
|
|
45
|
+
options.createConnection?.().destroy();
|
|
46
|
+
throw ex;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const promise = new Promise((resolve, reject) => {
|
|
50
|
+
let client, req;
|
|
51
|
+
|
|
52
|
+
if (options.h2) {
|
|
53
|
+
client = http2.connect(url.origin, options);
|
|
54
|
+
req = client.request(options.headers, options);
|
|
55
|
+
} else {
|
|
56
|
+
const { request } = url.protocol === 'http:' ? http : https;
|
|
57
|
+
|
|
58
|
+
req = request(url, options);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
affix(client, req, options);
|
|
62
|
+
|
|
63
|
+
req.once('error', reject);
|
|
64
|
+
req.once('frameError', reject);
|
|
65
|
+
req.once('goaway', reject);
|
|
66
|
+
req.once('response', (res) => postflight(req, res, options, {
|
|
67
|
+
reject,
|
|
68
|
+
resolve,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
dispatch(options, req);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const res = await promise;
|
|
76
|
+
|
|
77
|
+
if (digest && !redirected) {
|
|
78
|
+
res.body = await res.body();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return res;
|
|
82
|
+
} catch (ex) {
|
|
83
|
+
const { maxRetryAfter, retry } = options;
|
|
84
|
+
|
|
85
|
+
if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
|
|
86
|
+
let { interval } = retry;
|
|
87
|
+
|
|
88
|
+
if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
|
|
89
|
+
interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
|
|
90
|
+
interval = Number(interval) * 1000 || new Date(interval) - Date.now();
|
|
91
|
+
if (interval > maxRetryAfter) {
|
|
92
|
+
throw maxRetryAfterError(interval, { cause: ex });
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
retry.attempts--;
|
|
99
|
+
retry.interval = interval;
|
|
100
|
+
|
|
101
|
+
return setTimeoutPromise(interval).then(() => overact(url, options));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (digest && !redirected && ex.body) {
|
|
105
|
+
ex.body = await ex.body();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!thenable) {
|
|
109
|
+
throw ex;
|
|
110
|
+
} else {
|
|
111
|
+
return ex;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import http2 from 'node:http2';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
3
|
+
import { buffer } from 'node:stream/consumers';
|
|
4
|
+
import { types } from 'node:util';
|
|
5
|
+
import { File } from './file.mjs';
|
|
6
|
+
import { FormData } from './formdata.mjs';
|
|
7
|
+
import {
|
|
8
|
+
APPLICATION_FORM_URLENCODED,
|
|
9
|
+
APPLICATION_JSON,
|
|
10
|
+
APPLICATION_OCTET_STREAM,
|
|
11
|
+
} from './mediatypes.mjs';
|
|
12
|
+
import { compress } from './utils.mjs';
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
HTTP2_HEADER_CONTENT_ENCODING,
|
|
16
|
+
HTTP2_HEADER_CONTENT_LENGTH,
|
|
17
|
+
HTTP2_HEADER_CONTENT_TYPE,
|
|
18
|
+
} = http2.constants;
|
|
19
|
+
|
|
20
|
+
export const transform = async (options) => {
|
|
21
|
+
let { body, headers } = options;
|
|
22
|
+
|
|
23
|
+
if (!body) {
|
|
24
|
+
return options;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (File.alike(body)) {
|
|
28
|
+
headers = {
|
|
29
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: body.size,
|
|
30
|
+
[HTTP2_HEADER_CONTENT_TYPE]: body.type || APPLICATION_OCTET_STREAM,
|
|
31
|
+
};
|
|
32
|
+
body = body.stream();
|
|
33
|
+
} else if (FormData.alike(body)) {
|
|
34
|
+
body = FormData.actuate(body);
|
|
35
|
+
headers = { [HTTP2_HEADER_CONTENT_TYPE]: body.contentType };
|
|
36
|
+
} else if (!Buffer.isBuffer(body)) {
|
|
37
|
+
if (types.isAnyArrayBuffer(body)) {
|
|
38
|
+
body = Buffer.from(body);
|
|
39
|
+
} else if (types.isArrayBufferView(body)) {
|
|
40
|
+
body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
41
|
+
} else if (body === Object(body) && !Reflect.has(body, Symbol.asyncIterator)) {
|
|
42
|
+
if (body.constructor === URLSearchParams) {
|
|
43
|
+
headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_FORM_URLENCODED };
|
|
44
|
+
body = body.toString();
|
|
45
|
+
} else if (!(!Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
|
|
46
|
+
headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_JSON };
|
|
47
|
+
body = JSON.stringify(body);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const encodings = options.headers[HTTP2_HEADER_CONTENT_ENCODING];
|
|
53
|
+
|
|
54
|
+
if (body === Object(body)
|
|
55
|
+
&& (Reflect.has(body, Symbol.asyncIterator) || (!Array.isArray(body) && Reflect.has(body, Symbol.iterator)))) {
|
|
56
|
+
body = encodings ? compress(Readable.from(body), encodings) : Readable.from(body);
|
|
57
|
+
} else if (encodings) {
|
|
58
|
+
body = await buffer(compress(Readable.from(body), encodings));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Object.assign(options.headers, {
|
|
62
|
+
...headers,
|
|
63
|
+
...!body[Symbol.asyncIterator] && {
|
|
64
|
+
[HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body),
|
|
65
|
+
},
|
|
66
|
+
...options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
|
|
67
|
+
[HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...options,
|
|
73
|
+
body,
|
|
74
|
+
};
|
|
75
|
+
};
|
package/src/utils.mjs
CHANGED
|
@@ -1,34 +1,13 @@
|
|
|
1
|
-
import http from 'node:http';
|
|
2
1
|
import http2 from 'node:http2';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
pipeline,
|
|
6
|
-
Readable,
|
|
7
|
-
} from 'node:stream';
|
|
8
|
-
import { buffer } from 'node:stream/consumers';
|
|
9
|
-
import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
|
|
10
|
-
import { types } from 'node:util';
|
|
2
|
+
import { pipeline } from 'node:stream';
|
|
11
3
|
import zlib from 'node:zlib';
|
|
12
|
-
import { ackn } from './ackn.mjs';
|
|
13
4
|
import defaults from './defaults.mjs';
|
|
14
5
|
import {
|
|
15
6
|
RequestError,
|
|
16
7
|
TimeoutError,
|
|
17
8
|
} from './errors.mjs';
|
|
18
|
-
import { File } from './file.mjs';
|
|
19
|
-
import { FormData } from './formdata.mjs';
|
|
20
|
-
import {
|
|
21
|
-
APPLICATION_FORM_URLENCODED,
|
|
22
|
-
APPLICATION_JSON,
|
|
23
|
-
APPLICATION_OCTET_STREAM,
|
|
24
|
-
} from './mediatypes.mjs';
|
|
25
|
-
import { postflight } from './postflight.mjs';
|
|
26
|
-
import { preflight } from './preflight.mjs';
|
|
27
9
|
|
|
28
10
|
const {
|
|
29
|
-
HTTP2_HEADER_CONTENT_ENCODING,
|
|
30
|
-
HTTP2_HEADER_CONTENT_LENGTH,
|
|
31
|
-
HTTP2_HEADER_CONTENT_TYPE,
|
|
32
11
|
HTTP2_HEADER_RETRY_AFTER,
|
|
33
12
|
HTTP2_HEADER_STATUS,
|
|
34
13
|
} = http2.constants;
|
|
@@ -180,6 +159,10 @@ export const normalize = (url, options = {}) => {
|
|
|
180
159
|
url = `${ url }`.replace(/(?<!:)\/+/g, '/');
|
|
181
160
|
}
|
|
182
161
|
|
|
162
|
+
if (options.stripTrailingSlash) {
|
|
163
|
+
url = `${ url }`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
|
|
164
|
+
}
|
|
165
|
+
|
|
183
166
|
url = new URL(url, options.baseURL);
|
|
184
167
|
|
|
185
168
|
return Object.assign(options, { url });
|
|
@@ -197,157 +180,9 @@ export async function* tap(value) {
|
|
|
197
180
|
}
|
|
198
181
|
}
|
|
199
182
|
|
|
200
|
-
export const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (url.protocol === 'https:') {
|
|
208
|
-
options = !options.h2 ? await ackn(options) : {
|
|
209
|
-
...options,
|
|
210
|
-
createConnection: null,
|
|
211
|
-
protocol: url.protocol,
|
|
212
|
-
};
|
|
213
|
-
} else if (Reflect.has(options, 'alpnProtocol')) {
|
|
214
|
-
[
|
|
215
|
-
'alpnProtocol',
|
|
216
|
-
'createConnection',
|
|
217
|
-
'h2',
|
|
218
|
-
'protocol',
|
|
219
|
-
].forEach((it) => Reflect.deleteProperty(options, it));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
options = await transform(preflight(options));
|
|
224
|
-
} catch (ex) {
|
|
225
|
-
options.createConnection?.().destroy();
|
|
226
|
-
throw ex;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const promise = new Promise((resolve, reject) => {
|
|
230
|
-
let client, req;
|
|
231
|
-
|
|
232
|
-
if (options.h2) {
|
|
233
|
-
client = http2.connect(url.origin, options);
|
|
234
|
-
req = client.request(options.headers, options);
|
|
235
|
-
} else {
|
|
236
|
-
const { request } = url.protocol === 'http:' ? http : https;
|
|
237
|
-
|
|
238
|
-
req = request(url, options);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
affix(client, req, options);
|
|
242
|
-
|
|
243
|
-
req.once('error', reject);
|
|
244
|
-
req.once('frameError', reject);
|
|
245
|
-
req.once('goaway', reject);
|
|
246
|
-
req.once('response', (res) => postflight(req, res, options, {
|
|
247
|
-
reject,
|
|
248
|
-
resolve,
|
|
249
|
-
}));
|
|
250
|
-
|
|
251
|
-
dispatch(options, req);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
const res = await promise;
|
|
256
|
-
|
|
257
|
-
if (digest && !redirected) {
|
|
258
|
-
res.body = await res.body();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return res;
|
|
262
|
-
} catch (ex) {
|
|
263
|
-
const { maxRetryAfter, retry } = options;
|
|
264
|
-
|
|
265
|
-
if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
|
|
266
|
-
let { interval } = retry;
|
|
267
|
-
|
|
268
|
-
if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
|
|
269
|
-
interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
|
|
270
|
-
interval = Number(interval) * 1000 || new Date(interval) - Date.now();
|
|
271
|
-
if (interval > maxRetryAfter) {
|
|
272
|
-
throw maxRetryAfterError(interval, { cause: ex });
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
retry.attempts--;
|
|
279
|
-
retry.interval = interval;
|
|
280
|
-
|
|
281
|
-
return setTimeoutPromise(interval).then(() => overact(url, options));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (digest && !redirected && ex.body) {
|
|
285
|
-
ex.body = await ex.body();
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (!thenable) {
|
|
289
|
-
throw ex;
|
|
290
|
-
} else {
|
|
291
|
-
return ex;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
export const transform = async (options) => {
|
|
297
|
-
let { body, headers } = options;
|
|
298
|
-
|
|
299
|
-
if (!body) {
|
|
300
|
-
return options;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (File.alike(body)) {
|
|
304
|
-
headers = {
|
|
305
|
-
[HTTP2_HEADER_CONTENT_LENGTH]: body.size,
|
|
306
|
-
[HTTP2_HEADER_CONTENT_TYPE]: body.type || APPLICATION_OCTET_STREAM,
|
|
307
|
-
};
|
|
308
|
-
body = body.stream();
|
|
309
|
-
} else if (FormData.alike(body)) {
|
|
310
|
-
body = FormData.actuate(body);
|
|
311
|
-
headers = { [HTTP2_HEADER_CONTENT_TYPE]: body.contentType };
|
|
312
|
-
} else if (!Buffer.isBuffer(body)) {
|
|
313
|
-
if (types.isAnyArrayBuffer(body)) {
|
|
314
|
-
body = Buffer.from(body);
|
|
315
|
-
} else if (types.isArrayBufferView(body)) {
|
|
316
|
-
body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
317
|
-
} else if (body === Object(body) && !Reflect.has(body, Symbol.asyncIterator)) {
|
|
318
|
-
if (body.constructor === URLSearchParams) {
|
|
319
|
-
headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_FORM_URLENCODED };
|
|
320
|
-
body = body.toString();
|
|
321
|
-
} else if (!(!Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
|
|
322
|
-
headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_JSON };
|
|
323
|
-
body = JSON.stringify(body);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const encodings = options.headers[HTTP2_HEADER_CONTENT_ENCODING];
|
|
329
|
-
|
|
330
|
-
if (body === Object(body)
|
|
331
|
-
&& (Reflect.has(body, Symbol.asyncIterator) || (!Array.isArray(body) && Reflect.has(body, Symbol.iterator)))) {
|
|
332
|
-
body = encodings ? compress(Readable.from(body), encodings) : Readable.from(body);
|
|
333
|
-
} else if (encodings) {
|
|
334
|
-
body = await buffer(compress(Readable.from(body), encodings));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
Object.assign(options.headers, {
|
|
338
|
-
...headers,
|
|
339
|
-
...!body[Symbol.asyncIterator] && {
|
|
340
|
-
[HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body),
|
|
341
|
-
},
|
|
342
|
-
...options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
|
|
343
|
-
[HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE],
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
return {
|
|
348
|
-
...options,
|
|
349
|
-
body,
|
|
350
|
-
};
|
|
351
|
-
};
|
|
183
|
+
export const toCamelCase = (str) => str?.toLowerCase().replace(
|
|
184
|
+
/\p{Punctuation}.|\p{White_Space}./gu,
|
|
185
|
+
(val) => val.replace(/\p{Punctuation}+|\p{White_Space}+/gu, '').toUpperCase(),
|
|
186
|
+
);
|
|
352
187
|
|
|
353
188
|
export const unwind = (encodings) => encodings.split(',').map((it) => it.trim());
|