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/utils.mjs CHANGED
@@ -1,43 +1,45 @@
1
1
  import { Blob } from 'node:buffer';
2
+ import http from 'node:http';
2
3
  import http2 from 'node:http2';
4
+ import https from 'node:https';
3
5
  import {
4
6
  pipeline,
5
7
  Readable,
6
8
  } from 'node:stream';
7
9
  import { buffer } from 'node:stream/consumers';
10
+ import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
8
11
  import { types } from 'node:util';
9
12
  import zlib from 'node:zlib';
10
- import { redirectModes } from './constants.mjs';
11
- import { Cookies } from './cookies.mjs';
12
- import { TimeoutError } from './errors.mjs';
13
+ import { ackn } from './ackn.mjs';
14
+ import {
15
+ requestCredentials,
16
+ requestRedirect,
17
+ } from './constants.mjs';
18
+ import {
19
+ RequestError,
20
+ TimeoutError,
21
+ } from './errors.mjs';
13
22
  import { File } from './file.mjs';
14
23
  import { FormData } from './formdata.mjs';
24
+ import rekwest from './index.mjs';
15
25
  import {
16
26
  APPLICATION_FORM_URLENCODED,
17
27
  APPLICATION_JSON,
18
28
  APPLICATION_OCTET_STREAM,
19
- TEXT_PLAIN,
20
- WILDCARD,
21
29
  } from './mediatypes.mjs';
30
+ import { postflight } from './postflight.mjs';
31
+ import { preflight } from './preflight.mjs';
22
32
 
23
33
  const {
24
- HTTP2_HEADER_ACCEPT,
25
- HTTP2_HEADER_ACCEPT_ENCODING,
26
- HTTP2_HEADER_AUTHORITY,
27
34
  HTTP2_HEADER_CONTENT_ENCODING,
28
35
  HTTP2_HEADER_CONTENT_LENGTH,
29
36
  HTTP2_HEADER_CONTENT_TYPE,
30
- HTTP2_HEADER_COOKIE,
31
- HTTP2_HEADER_METHOD,
32
- HTTP2_HEADER_PATH,
33
- HTTP2_HEADER_SCHEME,
37
+ HTTP2_HEADER_RETRY_AFTER,
34
38
  HTTP2_HEADER_STATUS,
35
39
  HTTP2_METHOD_GET,
36
40
  HTTP2_METHOD_HEAD,
37
41
  } = http2.constants;
38
42
 
39
- const unwind = (encodings) => encodings.split(',').map((it) => it.trim());
40
-
41
43
  export const admix = (res, headers, options) => {
42
44
  const { h2 } = options;
43
45
 
@@ -138,6 +140,13 @@ export const dispatch = ({ body }, req) => {
138
140
  }
139
141
  };
140
142
 
143
+ export const maxRetryAfter = Symbol('maxRetryAfter');
144
+
145
+ export const maxRetryAfterError = (
146
+ interval,
147
+ options,
148
+ ) => new RequestError(`Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms.`, options);
149
+
141
150
  export const merge = (target = {}, ...rest) => {
142
151
  target = JSON.parse(JSON.stringify(target));
143
152
  if (!rest.length) {
@@ -246,7 +255,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
246
255
  if (/\bjson\b/i.test(contentType)) {
247
256
  body = JSON.parse(body.toString(charset));
248
257
  } else if (/\b(?:text|xml)\b/i.test(contentType)) {
249
- if (/\b(?:latin1|ucs-2|utf-(?:8|16le))\b/.test(charset)) {
258
+ if (/\b(?:latin1|ucs-2|utf-(?:8|16le))\b/i.test(charset)) {
250
259
  body = body.toString(charset);
251
260
  } else {
252
261
  body = new TextDecoder(charset).decode(body);
@@ -267,72 +276,9 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
267
276
  });
268
277
  };
269
278
 
270
- export const preflight = (options) => {
271
- const { cookies, h2 = false, headers, method = HTTP2_METHOD_GET, redirected, url } = options;
272
-
273
- if (h2) {
274
- options.endStream = [
275
- HTTP2_METHOD_GET,
276
- HTTP2_METHOD_HEAD,
277
- ].includes(method);
278
- }
279
-
280
- if (cookies !== false) {
281
- let cookie = Cookies.jar.get(url.origin);
282
-
283
- if (cookies === Object(cookies) && !redirected) {
284
- if (cookie) {
285
- new Cookies(cookies).forEach(function (val, key) {
286
- this.set(key, val);
287
- }, cookie);
288
- } else {
289
- cookie = new Cookies(cookies);
290
- Cookies.jar.set(url.origin, cookie);
291
- }
292
- }
293
-
294
- options.headers = {
295
- ...cookie && { [HTTP2_HEADER_COOKIE]: cookie },
296
- ...headers,
297
- };
298
- }
299
-
300
- options.digest ??= true;
301
- options.follow ??= 20;
302
- options.h2 ??= h2;
303
- options.headers = {
304
- [HTTP2_HEADER_ACCEPT]: `${ APPLICATION_JSON }, ${ TEXT_PLAIN }, ${ WILDCARD }`,
305
- [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, deflate-raw, gzip, identity',
306
- ...Object.entries(options.headers ?? {})
307
- .reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
308
- ...h2 && {
309
- [HTTP2_HEADER_AUTHORITY]: url.host,
310
- [HTTP2_HEADER_METHOD]: method,
311
- [HTTP2_HEADER_PATH]: `${ url.pathname }${ url.search }`,
312
- [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, ''),
313
- },
314
- };
315
-
316
- options.method ??= method;
317
- options.parse ??= true;
318
- options.redirect ??= redirectModes.follow;
319
-
320
- if (!Reflect.has(redirectModes, options.redirect)) {
321
- options.createConnection?.().destroy();
322
- throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${
323
- options.redirect
324
- }' is not a valid enum value.`);
325
- }
326
-
327
- options.redirected ??= false;
328
- options.thenable ??= false;
329
-
330
- return options;
331
- };
332
-
333
279
  export const sanitize = (url, options = {}) => {
334
280
  if (options.trimTrailingSlashes) {
335
- url = `${ url }`.replace(/(?<!:)\/+/gi, '/');
281
+ url = `${ url }`.replace(/(?<!:)\/+/g, '/');
336
282
  }
337
283
 
338
284
  url = new URL(url);
@@ -352,6 +298,99 @@ export async function* tap(value) {
352
298
  }
353
299
  }
354
300
 
301
+ export const transfer = async (options) => {
302
+ const { url } = options;
303
+
304
+ if (options.follow === 0) {
305
+ throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
306
+ }
307
+
308
+ if (url.protocol === 'https:') {
309
+ options = await ackn(options);
310
+ } else if (Reflect.has(options, 'alpnProtocol')) {
311
+ [
312
+ 'alpnProtocol',
313
+ 'createConnection',
314
+ 'h2',
315
+ 'protocol',
316
+ ].forEach((it) => Reflect.deleteProperty(options, it));
317
+ }
318
+
319
+ try {
320
+ options = await transform(preflight(options));
321
+ } catch (ex) {
322
+ options.createConnection?.().destroy();
323
+ throw ex;
324
+ }
325
+
326
+ const { digest, h2, redirected, thenable } = options;
327
+ const { request } = (url.protocol === 'http:' ? http : https);
328
+
329
+ const promise = new Promise((resolve, reject) => {
330
+ let client, req;
331
+
332
+ if (h2) {
333
+ client = http2.connect(url.origin, options);
334
+ req = client.request(options.headers, options);
335
+ } else {
336
+ req = request(url, options);
337
+ }
338
+
339
+ affix(client, req, options);
340
+
341
+ req.once('error', reject);
342
+ req.once('frameError', reject);
343
+ req.once('goaway', reject);
344
+ req.once('response', (res) => postflight(req, res, options, {
345
+ reject,
346
+ resolve,
347
+ }));
348
+
349
+ dispatch(options, req);
350
+ });
351
+
352
+ try {
353
+ const res = await promise;
354
+
355
+ if (digest && !redirected) {
356
+ res.body = await res.body();
357
+ }
358
+
359
+ return res;
360
+ } catch (ex) {
361
+ const { maxRetryAfter, retry } = options;
362
+
363
+ if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
364
+ let { interval } = retry;
365
+
366
+ if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
367
+ interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
368
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
369
+ if (interval > maxRetryAfter) {
370
+ throw maxRetryAfterError(interval, { cause: ex });
371
+ }
372
+ } else {
373
+ interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
374
+ }
375
+
376
+ retry.attempts--;
377
+ retry.interval = interval;
378
+
379
+ return setTimeoutPromise(interval).then(() => rekwest(url, options));
380
+ }
381
+
382
+ if (digest && !redirected && ex.body) {
383
+ ex.body = await ex.body();
384
+ }
385
+
386
+ if (!thenable) {
387
+ throw ex;
388
+ } else {
389
+ return ex;
390
+ }
391
+ }
392
+ };
393
+
355
394
  export const transform = async (options) => {
356
395
  let { body, headers } = options;
357
396
 
@@ -408,3 +447,28 @@ export const transform = async (options) => {
408
447
  body,
409
448
  };
410
449
  };
450
+
451
+ export const unwind = (encodings) => encodings.split(',').map((it) => it.trim());
452
+
453
+ export const validation = (options = {}) => {
454
+ if (options.body && [
455
+ HTTP2_METHOD_GET,
456
+ HTTP2_METHOD_HEAD,
457
+ ].includes(options.method)) {
458
+ throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body.`);
459
+ }
460
+
461
+ if (!Object.values(requestCredentials).includes(options.credentials)) {
462
+ throw new TypeError(`Failed to read the 'credentials' property from 'options': The provided value '${
463
+ options.credentials
464
+ }' is not a valid enum value.`);
465
+ }
466
+
467
+ if (!Reflect.has(requestRedirect, options.redirect)) {
468
+ throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${
469
+ options.redirect
470
+ }' is not a valid enum value.`);
471
+ }
472
+
473
+ return options;
474
+ };