rekwest 4.0.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,42 +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 { Cookies } from './cookies.mjs';
11
- 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';
12
22
  import { File } from './file.mjs';
13
23
  import { FormData } from './formdata.mjs';
24
+ import rekwest from './index.mjs';
14
25
  import {
15
26
  APPLICATION_FORM_URLENCODED,
16
27
  APPLICATION_JSON,
17
28
  APPLICATION_OCTET_STREAM,
18
- TEXT_PLAIN,
19
- WILDCARD,
20
29
  } from './mediatypes.mjs';
30
+ import { postflight } from './postflight.mjs';
31
+ import { preflight } from './preflight.mjs';
21
32
 
22
33
  const {
23
- HTTP2_HEADER_ACCEPT,
24
- HTTP2_HEADER_ACCEPT_ENCODING,
25
- HTTP2_HEADER_AUTHORITY,
26
34
  HTTP2_HEADER_CONTENT_ENCODING,
27
35
  HTTP2_HEADER_CONTENT_LENGTH,
28
36
  HTTP2_HEADER_CONTENT_TYPE,
29
- HTTP2_HEADER_COOKIE,
30
- HTTP2_HEADER_METHOD,
31
- HTTP2_HEADER_PATH,
32
- HTTP2_HEADER_SCHEME,
37
+ HTTP2_HEADER_RETRY_AFTER,
33
38
  HTTP2_HEADER_STATUS,
34
39
  HTTP2_METHOD_GET,
35
40
  HTTP2_METHOD_HEAD,
36
41
  } = http2.constants;
37
42
 
38
- const unwind = (encodings) => encodings.split(',').map((it) => it.trim());
39
-
40
43
  export const admix = (res, headers, options) => {
41
44
  const { h2 } = options;
42
45
 
@@ -79,8 +82,8 @@ export const affix = (client, req, options) => {
79
82
  });
80
83
  };
81
84
 
82
- export const collate = (entity, primordial) => {
83
- if (entity?.constructor !== primordial) {
85
+ export const brandCheck = (value, ctor) => {
86
+ if (!(value instanceof ctor)) {
84
87
  throw new TypeError('Illegal invocation');
85
88
  }
86
89
  };
@@ -137,6 +140,13 @@ export const dispatch = ({ body }, req) => {
137
140
  }
138
141
  };
139
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
+
140
150
  export const merge = (target = {}, ...rest) => {
141
151
  target = JSON.parse(JSON.stringify(target));
142
152
  if (!rest.length) {
@@ -174,7 +184,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
174
184
  arrayBuffer: {
175
185
  enumerable: true,
176
186
  value: async function () {
177
- collate(this, res?.constructor);
187
+ brandCheck(this, res?.constructor);
178
188
  parse &&= false;
179
189
  const { buffer, byteLength, byteOffset } = await this.body();
180
190
 
@@ -184,7 +194,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
184
194
  blob: {
185
195
  enumerable: true,
186
196
  value: async function () {
187
- collate(this, res?.constructor);
197
+ brandCheck(this, res?.constructor);
188
198
  const val = await this.arrayBuffer();
189
199
 
190
200
  return new Blob([val]);
@@ -193,7 +203,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
193
203
  json: {
194
204
  enumerable: true,
195
205
  value: async function () {
196
- collate(this, res?.constructor);
206
+ brandCheck(this, res?.constructor);
197
207
  const val = await this.text();
198
208
 
199
209
  return JSON.parse(val);
@@ -202,7 +212,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
202
212
  text: {
203
213
  enumerable: true,
204
214
  value: async function () {
205
- collate(this, res?.constructor);
215
+ brandCheck(this, res?.constructor);
206
216
  const blob = await this.blob();
207
217
 
208
218
  return blob.text();
@@ -215,7 +225,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
215
225
  body: {
216
226
  enumerable: true,
217
227
  value: async function () {
218
- collate(this, res?.constructor);
228
+ brandCheck(this, res?.constructor);
219
229
 
220
230
  if (this.bodyUsed) {
221
231
  throw new TypeError('Response stream already read');
@@ -245,7 +255,7 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
245
255
  if (/\bjson\b/i.test(contentType)) {
246
256
  body = JSON.parse(body.toString(charset));
247
257
  } else if (/\b(?:text|xml)\b/i.test(contentType)) {
248
- if (/\b(?:latin1|ucs-2|utf-(?:8|16le))\b/.test(charset)) {
258
+ if (/\b(?:latin1|ucs-2|utf-(?:8|16le))\b/i.test(charset)) {
249
259
  body = body.toString(charset);
250
260
  } else {
251
261
  body = new TextDecoder(charset).decode(body);
@@ -266,78 +276,9 @@ export const mixin = (res, { digest = false, parse = false } = {}) => {
266
276
  });
267
277
  };
268
278
 
269
- export const preflight = (options) => {
270
- const { cookies, h2 = false, headers, method = HTTP2_METHOD_GET, redirected, url } = options;
271
-
272
- if (h2) {
273
- options.endStream = [
274
- HTTP2_METHOD_GET,
275
- HTTP2_METHOD_HEAD,
276
- ].includes(method);
277
- }
278
-
279
- if (cookies !== false) {
280
- let cookie = Cookies.jar.get(url.origin);
281
-
282
- if (cookies === Object(cookies) && !redirected) {
283
- if (cookie) {
284
- new Cookies(cookies).forEach(function (val, key) {
285
- this.set(key, val);
286
- }, cookie);
287
- } else {
288
- cookie = new Cookies(cookies);
289
- Cookies.jar.set(url.origin, cookie);
290
- }
291
- }
292
-
293
- options.headers = {
294
- ...cookie && { [HTTP2_HEADER_COOKIE]: cookie },
295
- ...headers,
296
- };
297
- }
298
-
299
- options.digest ??= true;
300
- options.follow ??= 20;
301
- options.h2 ??= h2;
302
- options.headers = {
303
- [HTTP2_HEADER_ACCEPT]: `${ APPLICATION_JSON }, ${ TEXT_PLAIN }, ${ WILDCARD }`,
304
- [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, deflate-raw, gzip, identity',
305
- ...Object.entries(options.headers ?? {})
306
- .reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
307
- ...h2 && {
308
- [HTTP2_HEADER_AUTHORITY]: url.host,
309
- [HTTP2_HEADER_METHOD]: method,
310
- [HTTP2_HEADER_PATH]: `${ url.pathname }${ url.search }`,
311
- [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, ''),
312
- },
313
- };
314
-
315
- options.method ??= method;
316
- options.parse ??= true;
317
- options.redirect ??= redirects.follow;
318
-
319
- if (!Object.values(redirects).includes(options.redirect)) {
320
- options.createConnection?.().destroy();
321
- throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${
322
- options.redirect
323
- }' is not a valid enum value.`);
324
- }
325
-
326
- options.redirected ??= false;
327
- options.thenable ??= false;
328
-
329
- return options;
330
- };
331
-
332
- export const redirects = {
333
- error: 'error',
334
- follow: 'follow',
335
- manual: 'manual',
336
- };
337
-
338
279
  export const sanitize = (url, options = {}) => {
339
280
  if (options.trimTrailingSlashes) {
340
- url = `${ url }`.replace(/(?<!:)\/+/gi, '/');
281
+ url = `${ url }`.replace(/(?<!:)\/+/g, '/');
341
282
  }
342
283
 
343
284
  url = new URL(url);
@@ -345,6 +286,8 @@ export const sanitize = (url, options = {}) => {
345
286
  return Object.assign(options, { url });
346
287
  };
347
288
 
289
+ export const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
290
+
348
291
  export async function* tap(value) {
349
292
  if (Reflect.has(value, Symbol.asyncIterator)) {
350
293
  yield* value;
@@ -355,6 +298,99 @@ export async function* tap(value) {
355
298
  }
356
299
  }
357
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
+
358
394
  export const transform = async (options) => {
359
395
  let { body, headers } = options;
360
396
 
@@ -389,19 +425,18 @@ export const transform = async (options) => {
389
425
 
390
426
  const encodings = options.headers[HTTP2_HEADER_CONTENT_ENCODING];
391
427
 
392
- if (encodings) {
393
- if (Reflect.has(body, Symbol.asyncIterator)) {
394
- body = compress(Readable.from(body), encodings);
395
- } else {
396
- body = await buffer(compress(Readable.from(body), encodings));
397
- }
398
- } else if (body === Object(body)
428
+ if (body === Object(body)
399
429
  && (Reflect.has(body, Symbol.asyncIterator) || (!Array.isArray(body) && Reflect.has(body, Symbol.iterator)))) {
400
- body = Readable.from(body);
430
+ body = encodings ? compress(Readable.from(body), encodings) : Readable.from(body);
431
+ } else if (encodings) {
432
+ body = await buffer(compress(Readable.from(body), encodings));
401
433
  }
402
434
 
403
435
  Object.assign(options.headers, {
404
436
  ...headers,
437
+ ...!body[Symbol.asyncIterator] && {
438
+ [HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body),
439
+ },
405
440
  ...options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
406
441
  [HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE],
407
442
  },
@@ -412,3 +447,28 @@ export const transform = async (options) => {
412
447
  body,
413
448
  };
414
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
+ };