rekwest 4.1.0 → 4.2.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/src/index.mjs CHANGED
@@ -1,313 +1,84 @@
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 {
7
- redirectModes,
8
- redirectStatusCodes,
9
- } from './constants.mjs';
10
- import { Cookies } from './cookies.mjs';
11
- import { RequestError } from './errors.mjs';
12
- import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
13
- import {
14
- admix,
15
- affix,
16
- dispatch,
17
- merge,
18
- mixin,
19
- preflight,
20
- sameOrigin,
21
- sanitize,
22
- transform,
23
- } from './utils.mjs';
24
-
25
- export { constants } from 'node:http2';
26
-
27
- export * from './ackn.mjs';
28
- export * from './constants.mjs';
29
- export * from './cookies.mjs';
30
- export * from './errors.mjs';
31
- export * from './file.mjs';
32
- export * from './formdata.mjs';
33
- export * as mediatypes from './mediatypes.mjs';
34
- export * from './utils.mjs';
35
-
36
- const {
37
- HTTP2_HEADER_AUTHORIZATION,
38
- HTTP2_HEADER_CONTENT_TYPE,
39
- HTTP2_HEADER_LOCATION,
40
- HTTP2_HEADER_RETRY_AFTER,
41
- HTTP2_HEADER_SET_COOKIE,
42
- HTTP2_METHOD_GET,
43
- HTTP2_METHOD_HEAD,
44
- HTTP2_METHOD_POST,
45
- HTTP_STATUS_BAD_REQUEST,
46
- HTTP_STATUS_FOUND,
47
- HTTP_STATUS_MOVED_PERMANENTLY,
48
- HTTP_STATUS_SEE_OTHER,
49
- HTTP_STATUS_SERVICE_UNAVAILABLE,
50
- HTTP_STATUS_TOO_MANY_REQUESTS,
51
- } = http2.constants;
52
-
53
- const maxRetryAfter = Symbol('maxRetryAfter');
54
- const maxRetryAfterError = (
55
- interval,
56
- options,
57
- ) => new RequestError(`Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms.`, options);
58
- let defaults = {
59
- follow: 20,
60
- get maxRetryAfter() {
61
- return this[maxRetryAfter] ?? this.timeout;
62
- },
63
- set maxRetryAfter(value) {
64
- this[maxRetryAfter] = value;
65
- },
66
- method: HTTP2_METHOD_GET,
67
- retry: {
68
- attempts: 0,
69
- backoffStrategy: 'interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)',
70
- interval: 1e3,
71
- retryAfter: true,
72
- statusCodes: [
73
- HTTP_STATUS_TOO_MANY_REQUESTS,
74
- HTTP_STATUS_SERVICE_UNAVAILABLE,
75
- ],
76
- },
77
- timeout: 3e5,
78
- };
79
-
80
- export default async function rekwest(...args) {
81
- let options = sanitize(...args);
82
- const { url } = options;
83
-
84
- if (!options.redirected) {
85
- options = merge(rekwest.defaults, options);
86
- }
87
-
88
- if (options.body && [
89
- HTTP2_METHOD_GET,
90
- HTTP2_METHOD_HEAD,
91
- ].includes(options.method)) {
92
- throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body.`);
93
- }
94
-
95
- if (options.follow === 0) {
96
- throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
97
- }
98
-
99
- if (url.protocol === 'https:') {
100
- options = await ackn(options);
101
- } else if (Reflect.has(options, 'alpnProtocol')) {
102
- [
103
- 'alpnProtocol',
104
- 'createConnection',
105
- 'h2',
106
- 'protocol',
107
- ].forEach((it) => Reflect.deleteProperty(options, it));
108
- }
109
-
110
- options = await transform(preflight(options));
111
-
112
- const { cookies, digest, follow, h2, redirect, redirected, thenable } = options;
113
- const { request } = (url.protocol === 'http:' ? http : https);
114
-
115
- const promise = new Promise((resolve, reject) => {
116
- let client, req;
117
-
118
- if (h2) {
119
- client = http2.connect(url.origin, options);
120
- req = client.request(options.headers, options);
121
- } else {
122
- req = request(url, options);
123
- }
124
-
125
- affix(client, req, options);
126
- req.once('error', reject);
127
- req.once('frameError', reject);
128
- req.once('goaway', reject);
129
- req.once('response', (res) => {
130
- let headers;
131
-
132
- if (h2) {
133
- headers = res;
134
- res = req;
135
- } else {
136
- res.once('error', reject);
137
- }
138
-
139
- admix(res, headers, options);
140
-
141
- if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
142
- if (Cookies.jar.has(url.origin)) {
143
- new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]).forEach(function (val, key) {
144
- this.set(key, val);
145
- }, Cookies.jar.get(url.origin));
146
- } else {
147
- Cookies.jar.set(url.origin, new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]));
148
- }
149
- }
150
-
151
- Reflect.defineProperty(res, 'cookies', {
152
- enumerable: true,
153
- value: cookies !== false && Cookies.jar.has(url.origin)
154
- ? Cookies.jar.get(url.origin)
155
- : void 0,
156
- });
157
-
158
- const { statusCode } = res;
159
-
160
- if (follow && /3\d{2}/.test(statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
161
- if (!redirectStatusCodes.includes(statusCode)) {
162
- return res.emit('error', new RangeError(`Invalid status code: ${ statusCode }`));
163
- }
164
-
165
- if (redirect === redirectModes.error) {
166
- return res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
167
- }
168
-
169
- if (redirect === redirectModes.follow) {
170
- const location = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
171
-
172
- if (!/^https?:/.test(location.protocol)) {
173
- return res.emit('error', new RequestError('URL scheme must be "http" or "https".'));
174
- }
175
-
176
- if (!sameOrigin(location, url)) {
177
- Reflect.deleteProperty(options.headers, HTTP2_HEADER_AUTHORIZATION);
178
- location.password = location.username = '';
179
- }
180
-
181
- options.url = location;
182
-
183
- if (statusCode !== HTTP_STATUS_SEE_OTHER && options?.body?.pipe?.constructor === Function) {
184
- return res.emit('error', new RequestError(`Unable to ${ redirect } redirect with streamable body.`));
185
- }
186
-
187
- options.follow--;
188
-
189
- if (([
190
- HTTP_STATUS_MOVED_PERMANENTLY,
191
- HTTP_STATUS_FOUND,
192
- ].includes(statusCode) && request.method === HTTP2_METHOD_POST) || (statusCode === HTTP_STATUS_SEE_OTHER && ![
193
- HTTP2_METHOD_GET,
194
- HTTP2_METHOD_HEAD,
195
- ].includes(options.method))) {
196
- Object.keys(options.headers).filter((it) => /^content-/i.test(it))
197
- .forEach((it) => Reflect.deleteProperty(options.headers, it));
198
- options.body = null;
199
- options.method = HTTP2_METHOD_GET;
200
- }
201
-
202
- Reflect.set(options, 'redirected', true);
203
-
204
- if (statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
205
- let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
206
-
207
- interval = Number(interval) * 1000 || new Date(interval) - Date.now();
208
-
209
- if (interval > options.maxRetryAfter) {
210
- return res.emit('error', maxRetryAfterError(interval, { cause: mixin(res, options) }));
211
- }
212
-
213
- return setTimeoutPromise(interval).then(() => rekwest(options.url, options).then(resolve, reject));
214
- }
215
-
216
- return rekwest(options.url, options).then(resolve, reject);
217
- }
218
- }
219
-
220
- if (statusCode >= HTTP_STATUS_BAD_REQUEST) {
221
- return reject(mixin(res, options));
222
- }
223
-
224
- resolve(mixin(res, options));
225
- });
226
-
227
- dispatch(options, req);
228
- });
229
-
230
- try {
231
- const res = await promise;
232
-
233
- if (digest && !redirected) {
234
- res.body = await res.body();
235
- }
236
-
237
- return res;
238
- } catch (ex) {
239
- const { maxRetryAfter, retry } = options;
240
-
241
- if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
242
- let { interval } = retry;
243
-
244
- if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
245
- interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
246
- interval = Number(interval) * 1000 || new Date(interval) - Date.now();
247
- if (interval > maxRetryAfter) {
248
- throw maxRetryAfterError(interval, { cause: ex });
249
- }
250
- } else {
251
- interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
252
- }
253
-
254
- retry.attempts--;
255
- retry.interval = interval;
256
-
257
- return setTimeoutPromise(interval).then(() => rekwest(url, options));
258
- }
259
-
260
- if (digest && !redirected && ex.body) {
261
- ex.body = await ex.body();
262
- }
263
-
264
- if (!thenable) {
265
- throw ex;
266
- } else {
267
- return ex;
268
- }
269
- }
270
- }
271
-
272
- Reflect.defineProperty(rekwest, 'stream', {
273
- enumerable: true,
274
- value(...args) {
275
- const options = preflight({
276
- ...merge(rekwest.defaults, {
277
- headers: { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_OCTET_STREAM },
278
- }, sanitize(...args)),
279
- redirect: redirectModes.manual,
280
- });
281
-
282
- const { h2, url } = options;
283
- const { request } = (url.protocol === 'http:' ? http : https);
284
- let client, req;
285
-
286
- if (h2) {
287
- client = http2.connect(url.origin, options);
288
- req = client.request(options.headers, options);
289
- } else {
290
- req = request(url, options);
291
- }
292
-
293
- affix(client, req, options);
294
- req.once('response', (res) => {
295
- let headers;
296
-
297
- if (h2) {
298
- headers = res;
299
- res = req;
300
- }
301
-
302
- admix(res, headers, options);
303
- });
304
-
305
- return req;
306
- },
307
- });
308
-
309
- Reflect.defineProperty(rekwest, 'defaults', {
310
- enumerable: true,
311
- get() { return defaults; },
312
- set(value) { defaults = merge(defaults, value); },
313
- });
1
+ import http from 'node:http';
2
+ import http2 from 'node:http2';
3
+ import https from 'node:https';
4
+ import { requestRedirect } from './constants.mjs';
5
+ import defaults from './defaults.mjs';
6
+ import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
7
+ import { preflight } from './preflight.mjs';
8
+ import {
9
+ admix,
10
+ affix,
11
+ merge,
12
+ sanitize,
13
+ transfer,
14
+ validation,
15
+ } from './utils.mjs';
16
+
17
+ export { constants } from 'node:http2';
18
+
19
+ export * from './ackn.mjs';
20
+ export * from './constants.mjs';
21
+ export * from './cookies.mjs';
22
+ export * from './errors.mjs';
23
+ export * from './file.mjs';
24
+ export * from './formdata.mjs';
25
+ export * as mediatypes from './mediatypes.mjs';
26
+ export * from './utils.mjs';
27
+
28
+ const {
29
+ HTTP2_HEADER_CONTENT_TYPE,
30
+ } = http2.constants;
31
+
32
+ export default function rekwest(...args) {
33
+ let options = sanitize(...args);
34
+
35
+ if (!options.redirected) {
36
+ options = merge(rekwest.defaults, options);
37
+ }
38
+
39
+ return transfer(validation(options));
40
+ }
41
+
42
+ Reflect.defineProperty(rekwest, 'stream', {
43
+ enumerable: true,
44
+ value(...args) {
45
+ const options = preflight({
46
+ ...validation(merge(rekwest.defaults, {
47
+ headers: { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_OCTET_STREAM },
48
+ }, sanitize(...args))),
49
+ redirect: requestRedirect.manual,
50
+ });
51
+
52
+ const { h2, url } = options;
53
+ const { request } = (url.protocol === 'http:' ? http : https);
54
+ let client, req;
55
+
56
+ if (h2) {
57
+ client = http2.connect(url.origin, options);
58
+ req = client.request(options.headers, options);
59
+ } else {
60
+ req = request(url, options);
61
+ }
62
+
63
+ affix(client, req, options);
64
+
65
+ req.once('response', (res) => {
66
+ let headers;
67
+
68
+ if (h2) {
69
+ headers = res;
70
+ res = req;
71
+ }
72
+
73
+ admix(res, headers, options);
74
+ });
75
+
76
+ return req;
77
+ },
78
+ });
79
+
80
+ Reflect.defineProperty(rekwest, 'defaults', {
81
+ enumerable: true,
82
+ get() { return defaults.stash; },
83
+ set(value) { defaults.stash = merge(defaults.stash, value); },
84
+ });
@@ -0,0 +1,135 @@
1
+ import http2 from 'node:http2';
2
+ import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
3
+ import {
4
+ requestCredentials,
5
+ requestRedirect,
6
+ requestRedirectCodes,
7
+ } from './constants.mjs';
8
+ import { Cookies } from './cookies.mjs';
9
+ import { RequestError } from './errors.mjs';
10
+ import rekwest from './index.mjs';
11
+ import {
12
+ admix,
13
+ maxRetryAfterError,
14
+ mixin,
15
+ sameOrigin,
16
+ } from './utils.mjs';
17
+
18
+ const {
19
+ HTTP2_HEADER_AUTHORIZATION,
20
+ HTTP2_HEADER_LOCATION,
21
+ HTTP2_HEADER_RETRY_AFTER,
22
+ HTTP2_HEADER_SET_COOKIE,
23
+ HTTP2_METHOD_GET,
24
+ HTTP2_METHOD_HEAD,
25
+ HTTP2_METHOD_POST,
26
+ HTTP_STATUS_BAD_REQUEST,
27
+ HTTP_STATUS_FOUND,
28
+ HTTP_STATUS_MOVED_PERMANENTLY,
29
+ HTTP_STATUS_SEE_OTHER,
30
+ } = http2.constants;
31
+
32
+ export const postflight = (req, res, options, { reject, resolve }) => {
33
+ const { cookies, credentials, follow, h2, redirect, url } = options;
34
+ let headers;
35
+
36
+ if (h2) {
37
+ headers = res;
38
+ res = req;
39
+ } else {
40
+ res.once('error', reject);
41
+ }
42
+
43
+ admix(res, headers, options);
44
+
45
+ if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
46
+ if (Cookies.jar.has(url.origin)) {
47
+ new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]).forEach(function (val, key) {
48
+ this.set(key, val);
49
+ }, Cookies.jar.get(url.origin));
50
+ } else {
51
+ Cookies.jar.set(url.origin, new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]));
52
+ }
53
+ }
54
+
55
+ Reflect.defineProperty(res, 'cookies', {
56
+ enumerable: true,
57
+ value: cookies !== false && Cookies.jar.has(url.origin)
58
+ ? Cookies.jar.get(url.origin)
59
+ : void 0,
60
+ });
61
+
62
+ const { statusCode } = res;
63
+
64
+ if (follow && /3\d{2}/.test(statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
65
+ if (!requestRedirectCodes.includes(statusCode)) {
66
+ return res.emit('error', new RangeError(`Invalid status code: ${ statusCode }`));
67
+ }
68
+
69
+ if (redirect === requestRedirect.error) {
70
+ return res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
71
+ }
72
+
73
+ if (redirect === requestRedirect.follow) {
74
+ const location = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
75
+
76
+ if (!/^https?:/i.test(location.protocol)) {
77
+ return res.emit('error', new RequestError('URL scheme must be "http" or "https".'));
78
+ }
79
+
80
+ if (!sameOrigin(location, url) && [
81
+ requestCredentials.omit,
82
+ requestCredentials.sameOrigin,
83
+ ].includes(credentials)) {
84
+ Reflect.deleteProperty(options.headers, HTTP2_HEADER_AUTHORIZATION);
85
+ location.password = location.username = '';
86
+ if (credentials === requestCredentials.omit) {
87
+ options.cookies = false;
88
+ }
89
+ }
90
+
91
+ options.url = location;
92
+
93
+ if (statusCode !== HTTP_STATUS_SEE_OTHER && options.body?.pipe?.constructor === Function) {
94
+ return res.emit('error', new RequestError(`Unable to ${ redirect } redirect with streamable body.`));
95
+ }
96
+
97
+ options.follow--;
98
+
99
+ if (([
100
+ HTTP_STATUS_MOVED_PERMANENTLY,
101
+ HTTP_STATUS_FOUND,
102
+ ].includes(statusCode) && options.method === HTTP2_METHOD_POST) || (statusCode === HTTP_STATUS_SEE_OTHER && ![
103
+ HTTP2_METHOD_GET,
104
+ HTTP2_METHOD_HEAD,
105
+ ].includes(options.method))) {
106
+ Object.keys(options.headers).filter((it) => /^content-/i.test(it))
107
+ .forEach((it) => Reflect.deleteProperty(options.headers, it));
108
+ options.body = null;
109
+ options.method = HTTP2_METHOD_GET;
110
+ }
111
+
112
+ Reflect.set(options, 'redirected', true);
113
+
114
+ if (statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
115
+ let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
116
+
117
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
118
+
119
+ if (interval > options.maxRetryAfter) {
120
+ return res.emit('error', maxRetryAfterError(interval, { cause: mixin(res, options) }));
121
+ }
122
+
123
+ return setTimeoutPromise(interval).then(() => rekwest(options.url, options).then(resolve, reject));
124
+ }
125
+
126
+ return rekwest(options.url, options).then(resolve, reject);
127
+ }
128
+ }
129
+
130
+ if (statusCode >= HTTP_STATUS_BAD_REQUEST) {
131
+ return reject(mixin(res, options));
132
+ }
133
+
134
+ resolve(mixin(res, options));
135
+ };
@@ -0,0 +1,70 @@
1
+ import http2 from 'node:http2';
2
+ import { requestCredentials } from './constants.mjs';
3
+ import { Cookies } from './cookies.mjs';
4
+ import {
5
+ APPLICATION_JSON,
6
+ TEXT_PLAIN,
7
+ WILDCARD,
8
+ } from './mediatypes.mjs';
9
+
10
+ const {
11
+ HTTP2_HEADER_ACCEPT,
12
+ HTTP2_HEADER_ACCEPT_ENCODING,
13
+ HTTP2_HEADER_AUTHORITY,
14
+ HTTP2_HEADER_COOKIE,
15
+ HTTP2_HEADER_METHOD,
16
+ HTTP2_HEADER_PATH,
17
+ HTTP2_HEADER_SCHEME,
18
+ HTTP2_METHOD_GET,
19
+ HTTP2_METHOD_HEAD,
20
+ } = http2.constants;
21
+
22
+ export const preflight = (options) => {
23
+ const { cookies, credentials, h2 = false, headers, method, url } = options;
24
+
25
+ if (h2) {
26
+ options.endStream = [
27
+ HTTP2_METHOD_GET,
28
+ HTTP2_METHOD_HEAD,
29
+ ].includes(method);
30
+ }
31
+
32
+ if (cookies !== false) {
33
+ let cookie = Cookies.jar.get(url.origin);
34
+
35
+ if (cookies === Object(cookies) && [
36
+ requestCredentials.include,
37
+ requestCredentials.sameOrigin,
38
+ ].includes(credentials)) {
39
+ if (cookie) {
40
+ new Cookies(cookies).forEach(function (val, key) {
41
+ this.set(key, val);
42
+ }, cookie);
43
+ } else {
44
+ cookie = new Cookies(cookies);
45
+ Cookies.jar.set(url.origin, cookie);
46
+ }
47
+ }
48
+
49
+ options.headers = {
50
+ ...cookie && { [HTTP2_HEADER_COOKIE]: cookie },
51
+ ...headers,
52
+ };
53
+ }
54
+
55
+ options.h2 ??= h2;
56
+ options.headers = {
57
+ [HTTP2_HEADER_ACCEPT]: `${ APPLICATION_JSON }, ${ TEXT_PLAIN }, ${ WILDCARD }`,
58
+ [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, deflate-raw, gzip, identity',
59
+ ...Object.entries(options.headers ?? {})
60
+ .reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
61
+ ...h2 && {
62
+ [HTTP2_HEADER_AUTHORITY]: url.host,
63
+ [HTTP2_HEADER_METHOD]: method,
64
+ [HTTP2_HEADER_PATH]: `${ url.pathname }${ url.search }`,
65
+ [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, ''),
66
+ },
67
+ };
68
+
69
+ return options;
70
+ };