rekwest 7.1.0 → 7.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/README.md CHANGED
@@ -49,8 +49,7 @@ const res = await rekwest(url, {
49
49
  headers: {
50
50
  [HTTP2_HEADER_AUTHORIZATION]: 'Bearer [token]',
51
51
  [HTTP2_HEADER_CONTENT_ENCODING]: 'br', // enables: body encoding
52
- /** [HTTP2_HEADER_CONTENT_TYPE]
53
- * is undue for
52
+ /** [HTTP2_HEADER_CONTENT_TYPE] is undue for
54
53
  * Array/Blob/File/FormData/Object/URLSearchParams body types
55
54
  * and will be set automatically, with an option to override it here
56
55
  */
@@ -82,7 +81,7 @@ const {
82
81
  } = constants;
83
82
 
84
83
  const blob = new Blob(['bits']);
85
- const file = new File(['bits'], 'file.dab');
84
+ const file = new File(['bits'], 'file.xyz');
86
85
  const readable = Readable.from('bits');
87
86
 
88
87
  const fd = new FormData({
@@ -90,9 +89,9 @@ const fd = new FormData({
90
89
  });
91
90
 
92
91
  fd.append('celestial', 'payload');
93
- fd.append('blob', blob, 'blob.dab');
92
+ fd.append('blob', blob, 'blob.xyz');
94
93
  fd.append('file', file);
95
- fd.append('readable', readable, 'readable.dab');
94
+ fd.append('readable', readable, 'readable.xyz');
96
95
 
97
96
  const url = 'https://somewhe.re/somewhat/endpoint';
98
97
 
@@ -121,36 +120,37 @@ console.log(res.body);
121
120
  & [http2.ClientSessionRequestOptions](https://nodejs.org/api/http2.html#clienthttp2sessionrequestheaders-options)
122
121
  and [tls.ConnectionOptions](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)
123
122
  for HTTP/2 attunes
123
+ * `allowDowngrade` **{boolean}** `Default: false` Controls whether `https:` redirects to `http:` are allowed
124
124
  * `baseURL` **{string | URL}** The base URL to use in cases where `url` is a relative URL
125
125
  * `body` **{string | Array | ArrayBuffer | ArrayBufferView | AsyncIterator | Blob | Buffer | DataView | File |
126
126
  FormData | Iterator | Object | Readable | ReadableStream | SharedArrayBuffer | URLSearchParams}** The body to send
127
127
  with the request
128
128
  * `bufferBody` **{boolean}** `Default: false` Toggles the buffering of the streamable request bodies for redirects and
129
129
  retries
130
- * `cookies` **{boolean | Array<[k, v]> | Array<string\> | Cookies | Object | URLSearchParams}** `Default: true` The
130
+ * `cookies` **{boolean | string[] | Array<[k, v]> | Cookies | Object | URLSearchParams}** `Default: true` The
131
131
  cookies to add to
132
132
  the request
133
133
  * `cookiesTTL` **{boolean}** `Default: false` Controls enablement of TTL for the cookies cache
134
134
  * `credentials` **{include | omit | same-origin}** `Default: same-origin` Controls credentials in case of cross-origin
135
135
  redirects
136
- * `decodersOptions` **{Object}** Configures decoders options, e.g.: `brotli`, `zstd`, `zlib`
136
+ * `decodersOptions` **{Object}** Configures decoders options, e.g.: `brotli`, `zlib`, `zstd`
137
137
  * `digest` **{boolean}** `Default: true` Controls whether to read the response stream or add a mixin
138
- * `encodersOptions` **{Object}** Configures encoders options, e.g.: `brotli`, `zstd`, `zlib`
138
+ * `encodersOptions` **{Object}** Configures encoders options, e.g.: `brotli`, `zlib`, `zstd`
139
139
  * `follow` **{number}** `Default: 20` The number of redirects to follow
140
140
  * `h2` **{boolean}** `Default: false` Forces the use of HTTP/2 protocol
141
141
  * `headers` **{Object}** The headers to add to the request
142
- * `maxRetryAfter` **{number}** The upper limit of `retry-after` header. If unset, it will use `timeout` value
143
142
  * `params` **{Object}** The search params to add to the `url`
144
143
  * `parse` **{boolean}** `Default: true` Controls whether to parse response body or return a buffer
145
144
  * `redirect` **{error | follow | manual}** `Default: follow` Controls the redirect flows
146
145
  * `retry` **{Object}** Represents the retry options
147
146
  * `attempts` **{number}** `Default: 0` The number of retry attempts
148
147
  * `backoffStrategy` **{string}** `Default: interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)`
149
- The backoff strategy algorithm that increases logarithmically. To fixate set value to `interval * 1`
148
+ The backoff strategy uses a log-uniform algorithm. To fix the interval, set the value to `interval * 1`.
150
149
  * `errorCodes` **{string[]}**
151
150
  `Default: ['ECONNREFUSED', 'ECONNRESET', 'EHOSTDOWN', 'EHOSTUNREACH', 'ENETDOWN', 'ENETUNREACH', 'ENOTFOUND', 'ERR_HTTP2_STREAM_ERROR']`
152
151
  The list of error codes to retry on
153
152
  * `interval` **{number}** `Default: 1e3` The initial retry interval
153
+ * `maxRetryAfter` **{number}** `Default: 3e5` The maximum `retry-after` limit in milliseconds
154
154
  * `retryAfter` **{boolean}** `Default: true` Controls `retry-after` header receptiveness
155
155
  * `statusCodes` **{number[]}** `Default: [429, 500, 502, 503, 504]` The list of status codes to retry on
156
156
  * `stripTrailingSlash` **{boolean}** `Default: false` Controls whether to strip trailing slash at the end of the URL
@@ -202,10 +202,16 @@ const rk = rekwest.extend({
202
202
  baseURL: 'https://somewhe.re',
203
203
  });
204
204
 
205
+ const params = {
206
+ id: '[uid]',
207
+ signature: '[code]',
208
+ variant: 'A',
209
+ };
205
210
  const signal = AbortSignal.timeout(1e4);
206
211
  const url = '/somewhat/endpoint';
207
212
 
208
213
  const res = await rk(url, {
214
+ params,
209
215
  signal,
210
216
  });
211
217
 
package/dist/config.cjs CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  } = _nodeHttp.default.constants;
23
23
  const timeout = 3e5;
24
24
  const defaults = {
25
+ allowDowngrade: false,
25
26
  bufferBody: false,
26
27
  cookiesTTL: false,
27
28
  credentials: _constants.requestCredentials.sameOrigin,
@@ -45,16 +46,15 @@ const defaults = {
45
46
  [HTTP2_HEADER_ACCEPT]: `${_mediatypes.APPLICATION_JSON}, ${_mediatypes.TEXT_PLAIN}, ${_mediatypes.WILDCARD}`,
46
47
  [HTTP2_HEADER_ACCEPT_ENCODING]: `br,${isZstdSupported ? ' zstd, ' : ' '}gzip, deflate, deflate-raw`
47
48
  },
48
- maxRetryAfter: timeout,
49
49
  method: HTTP2_METHOD_GET,
50
50
  parse: true,
51
51
  redirect: _constants.requestRedirect.follow,
52
- redirected: false,
53
52
  retry: {
54
53
  attempts: 0,
55
54
  backoffStrategy: 'interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)',
56
55
  errorCodes: ['ECONNREFUSED', 'ECONNRESET', 'EHOSTDOWN', 'EHOSTUNREACH', 'ENETDOWN', 'ENETUNREACH', 'ENOTFOUND', 'ERR_HTTP2_STREAM_ERROR'],
57
56
  interval: 1e3,
57
+ maxRetryAfter: timeout,
58
58
  retryAfter: true,
59
59
  statusCodes: [HTTP_STATUS_TOO_MANY_REQUESTS, HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_BAD_GATEWAY, HTTP_STATUS_SERVICE_UNAVAILABLE, HTTP_STATUS_GATEWAY_TIMEOUT]
60
60
  },
package/dist/cookies.cjs CHANGED
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.Cookies = void 0;
7
7
  var _utils = require("./utils.cjs");
8
- const lifetimeCap = 3456e7; // pragma: 400 days
8
+ const lifetimeCap = 3456e7; // 400 days
9
9
 
10
10
  class Cookies extends URLSearchParams {
11
11
  static #finalizers = new Set();
@@ -37,7 +37,7 @@ class Cookies extends URLSearchParams {
37
37
  for (const attr of attrs) {
38
38
  if (/(?:expires|max-age)=/i.test(attr)) {
39
39
  const [key, val] = attr.toLowerCase().split('=');
40
- const ms = Number.isFinite(Number(val)) ? val * 1e3 : Date.parse(val) - Date.now();
40
+ const ms = Number.isFinite(Number.parseInt(val, 10)) ? val * 1e3 : Date.parse(val) - Date.now();
41
41
  ttl[(0, _utils.toCamelCase)(key)] = Math.min(ms, lifetimeCap);
42
42
  }
43
43
  }
package/dist/formdata.cjs CHANGED
@@ -70,9 +70,9 @@ class FormData {
70
70
  if (Object(input) === input) {
71
71
  if (Array.isArray(input)) {
72
72
  if (!input.every(it => Array.isArray(it))) {
73
- throw new TypeError(`Failed to construct '${this[Symbol.toStringTag]}': The provided value cannot be converted to a sequence.`);
73
+ throw new TypeError(`Failed to construct '${this[Symbol.toStringTag]}': The provided value cannot be converted to a sequence`);
74
74
  } else if (!input.every(it => it.length === 2)) {
75
- throw new TypeError(`Failed to construct '${this[Symbol.toStringTag]}': Sequence initializer must only contain pair elements.`);
75
+ throw new TypeError(`Failed to construct '${this[Symbol.toStringTag]}': Sequence initializer must only contain pair elements`);
76
76
  }
77
77
  input = Array.from(input);
78
78
  } else if (!Reflect.has(input, Symbol.iterator)) {
@@ -85,16 +85,16 @@ class FormData {
85
85
  }
86
86
  #ensureArgs(args, expected, method) {
87
87
  if (args.length < expected) {
88
- throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': ${expected} arguments required, but only ${args.length} present.`);
88
+ throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': ${expected} arguments required, but only ${args.length} present`);
89
89
  }
90
90
  if (['append', 'set'].includes(method)) {
91
91
  if (args.length === 3 && !this.constructor.#ensureInstance(args[1])) {
92
- throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expected} is not of type 'Blob'.`);
92
+ throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expected} is not of type 'Blob'`);
93
93
  }
94
94
  }
95
95
  if (method === 'forEach') {
96
96
  if (args[0]?.constructor !== Function) {
97
- throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expected} is not of type 'Function'.`);
97
+ throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expected} is not of type 'Function'`);
98
98
  }
99
99
  }
100
100
  }
package/dist/mixin.cjs CHANGED
@@ -74,7 +74,7 @@ const mixin = (res, {
74
74
  value: async function () {
75
75
  (0, _utils.brandCheck)(this, res?.constructor);
76
76
  if (this.bodyUsed) {
77
- throw new TypeError('Response stream already read.');
77
+ throw new TypeError('Response stream already read');
78
78
  }
79
79
  let body = await (0, _consumers.buffer)((0, _codecs.decode)(this, this.headers[HTTP2_HEADER_CONTENT_ENCODING], {
80
80
  decodersOptions
@@ -48,13 +48,16 @@ const postflight = (req, res, options, {
48
48
  enumerable: true,
49
49
  value: cookies !== false && _cookies.Cookies.jar.has(url.origin) ? _cookies.Cookies.jar.get(url.origin) : void 0
50
50
  });
51
- const result = (0, _redirects.redirects)(res, options);
51
+ let result;
52
+ try {
53
+ result = (0, _redirects.redirects)(res, options);
54
+ } catch (err) {
55
+ res.emit('error', err);
56
+ return reject((0, _mixin.mixin)(res, options));
57
+ }
52
58
  if (Object(result) === result) {
53
59
  return result.then(resolve, reject);
54
60
  }
55
- if (result) {
56
- return reject((0, _mixin.mixin)(res, options));
57
- }
58
61
  if (res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
59
62
  return reject((0, _mixin.mixin)(res, options));
60
63
  }
@@ -24,6 +24,7 @@ const {
24
24
  } = _nodeHttp.default.constants;
25
25
  const redirects = (res, options) => {
26
26
  const {
27
+ allowDowngrade,
27
28
  credentials,
28
29
  follow,
29
30
  redirect,
@@ -31,21 +32,24 @@ const redirects = (res, options) => {
31
32
  } = options;
32
33
  if (follow && /3\d{2}/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
33
34
  if (redirect === _constants.requestRedirect.error) {
34
- return res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'.`));
35
+ throw new _errors.RequestError(`Unexpected redirect, redirect mode is set to: ${redirect}`);
35
36
  }
36
37
  if (redirect === _constants.requestRedirect.follow) {
37
- const location = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
38
- if (!/^https?:/i.test(location.protocol)) {
39
- return res.emit('error', new _errors.RequestError('URL scheme must be "http" or "https".'));
38
+ const loc = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
39
+ if (!/^https?:/i.test(loc.protocol)) {
40
+ throw new _errors.RequestError('URL scheme must be "http" or "https"');
40
41
  }
41
- if (!(0, _utils.sameOrigin)(location, url)) {
42
+ if (!allowDowngrade && loc.protocol === 'http:' && url.protocol === 'https:') {
43
+ throw new _errors.RequestError(`Protocol downgrade detected, redirect from "${url.protocol}" to "${loc.protocol}": ${loc}`);
44
+ }
45
+ if (!(0, _utils.sameOrigin)(loc, url)) {
42
46
  if (credentials !== _constants.requestCredentials.include) {
43
47
  options.credentials = _constants.requestCredentials.omit;
44
48
  }
45
49
  options.h2 = false;
46
50
  }
47
51
  if ([HTTP_STATUS_PERMANENT_REDIRECT, HTTP_STATUS_TEMPORARY_REDIRECT].includes(res.statusCode) && (0, _utils.isPipeStream)(options.body) && !(0, _nodeStream.isReadable)(options.body)) {
48
- return res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with streamable body.`));
52
+ throw new _errors.RequestError(`Unable to ${redirect} redirect with streamable body`);
49
53
  }
50
54
  if ([HTTP_STATUS_MOVED_PERMANENTLY, HTTP_STATUS_FOUND].includes(res.statusCode) && options.method === HTTP2_METHOD_POST || res.statusCode === HTTP_STATUS_SEE_OTHER && ![HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(options.method)) {
51
55
  options.body = null;
@@ -53,7 +57,7 @@ const redirects = (res, options) => {
53
57
  }
54
58
  options.follow--;
55
59
  options.redirected = true;
56
- return (0, _index.default)(location, options);
60
+ return (0, _index.default)(loc, options);
57
61
  }
58
62
  }
59
63
  };
package/dist/retries.cjs CHANGED
@@ -19,14 +19,13 @@ const {
19
19
  const retries = (err, options) => {
20
20
  const {
21
21
  body,
22
- maxRetryAfter,
23
22
  method,
24
23
  retry,
25
24
  url
26
25
  } = options;
27
26
  if (retry?.attempts > 0) {
28
27
  if (![HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(method) && (0, _utils.isPipeStream)(body) && !(0, _nodeStream.isReadable)(body)) {
29
- throw new _errors.RequestError('Request stream already read.', {
28
+ throw new _errors.RequestError('Request stream already read', {
30
29
  cause: err
31
30
  });
32
31
  }
@@ -36,9 +35,9 @@ const retries = (err, options) => {
36
35
  } = retry;
37
36
  if (retry.retryAfter && err.headers?.[HTTP2_HEADER_RETRY_AFTER]) {
38
37
  interval = err.headers[HTTP2_HEADER_RETRY_AFTER];
39
- interval = Math.abs(Number(interval) * 1e3 || new Date(interval) - Date.now()) || 0;
40
- if (interval > maxRetryAfter) {
41
- throw new _errors.RequestError(`Maximum '${HTTP2_HEADER_RETRY_AFTER}' limit exceeded: ${interval} ms.`, {
38
+ interval = Number.isFinite(Number.parseInt(interval, 10)) ? interval * 1e3 : new Date(interval) - Date.now();
39
+ if (interval > retry.maxRetryAfter) {
40
+ throw new _errors.RequestError(`Maximum '${HTTP2_HEADER_RETRY_AFTER}' limit exceeded: ${interval} ms`, {
42
41
  cause: err
43
42
  });
44
43
  }
@@ -50,7 +49,10 @@ const retries = (err, options) => {
50
49
  }
51
50
  retry.attempts--;
52
51
  retry.interval = interval;
53
- return (0, _promises.setTimeout)(interval).then(() => (0, _index.default)(url, options));
52
+ return _promises.scheduler.wait(interval).then(() => (0, _index.default)(url, {
53
+ ...options,
54
+ params: void 0
55
+ }));
54
56
  }
55
57
  }
56
58
  };
package/dist/utils.cjs CHANGED
@@ -60,7 +60,7 @@ const augment = (res, headers, options) => {
60
60
  exports.augment = augment;
61
61
  const brandCheck = (val, ctor) => {
62
62
  if (!(val instanceof ctor)) {
63
- throw new TypeError('Illegal invocation.');
63
+ throw new TypeError('Illegal invocation');
64
64
  }
65
65
  };
66
66
  exports.brandCheck = brandCheck;
@@ -127,15 +127,15 @@ const normalize = (url, options = {}) => {
127
127
  });
128
128
  };
129
129
  exports.normalize = normalize;
130
- const normalizeHeaders = headers => {
130
+ const normalizeHeaders = (headers = {}) => {
131
131
  const acc = {};
132
- for (const [key, val] of Object.entries(headers ?? {})) {
132
+ for (const [key, val] of Object.entries(headers)) {
133
133
  const name = key.toLowerCase();
134
134
  acc[key] = val;
135
135
  if (key === HTTP2_HEADER_ACCEPT_ENCODING && !_config.isZstdSupported) {
136
- const stripped = val.replace(/\s?zstd,?/gi, '').trim();
137
- if (stripped) {
138
- acc[key] = stripped;
136
+ const modified = val.replace(/\s?zstd,?/gi, '').trim();
137
+ if (modified) {
138
+ acc[key] = modified;
139
139
  } else {
140
140
  Reflect.deleteProperty(acc, name);
141
141
  }
@@ -149,7 +149,7 @@ exports.sameOrigin = sameOrigin;
149
149
  const snoop = (client, req, options) => {
150
150
  req.once('close', () => client?.close());
151
151
  req.once('end', () => client?.close());
152
- req.once('timeout', () => req.destroy(new _errors.TimeoutError(`Timed out after ${options.timeout} ms.`)));
152
+ req.once('timeout', () => req.destroy(new _errors.TimeoutError(`Timed out after ${options.timeout} ms`)));
153
153
  req.once('trailers', trailers => {
154
154
  Reflect.defineProperty(req, 'trailers', {
155
155
  enumerable: true,
@@ -13,13 +13,13 @@ const {
13
13
  } = _nodeHttp.default.constants;
14
14
  const validation = (options = {}) => {
15
15
  if (options.body && [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(options.method)) {
16
- throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body.`);
16
+ throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body`);
17
17
  }
18
18
  if (!Object.values(_constants.requestCredentials).includes(options.credentials)) {
19
- throw new TypeError(`Failed to read the 'credentials' property from 'options': The provided value '${options.credentials}' is not a valid enum value.`);
19
+ throw new TypeError(`Failed to read the 'credentials' property from 'options': The provided value '${options.credentials}' is not a valid enum value`);
20
20
  }
21
21
  if (!Reflect.has(_constants.requestRedirect, options.redirect)) {
22
- throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${options.redirect}' is not a valid enum value.`);
22
+ throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${options.redirect}' is not a valid enum value`);
23
23
  }
24
24
  return options;
25
25
  };
package/package.json CHANGED
@@ -71,5 +71,5 @@
71
71
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
72
72
  },
73
73
  "type": "module",
74
- "version": "7.1.0"
74
+ "version": "7.2.0"
75
75
  }
package/src/codecs.js CHANGED
@@ -36,6 +36,7 @@ export const encodeCodecs = {
36
36
  gzip: (opts) => zlib.createGzip(opts?.zlib),
37
37
  zstd: (opts) => isZstdSupported && zlib.createZstdCompress(opts?.zstd),
38
38
  };
39
+
39
40
  export const encode = (readable, encodings = '', { encodersOptions } = {}) => {
40
41
  const encoders = [];
41
42
 
package/src/config.js CHANGED
@@ -26,6 +26,7 @@ const {
26
26
  const timeout = 3e5;
27
27
 
28
28
  const defaults = {
29
+ allowDowngrade: false,
29
30
  bufferBody: false,
30
31
  cookiesTTL: false,
31
32
  credentials: requestCredentials.sameOrigin,
@@ -49,11 +50,9 @@ const defaults = {
49
50
  [HTTP2_HEADER_ACCEPT]: `${ APPLICATION_JSON }, ${ TEXT_PLAIN }, ${ WILDCARD }`,
50
51
  [HTTP2_HEADER_ACCEPT_ENCODING]: `br,${ isZstdSupported ? ' zstd, ' : ' ' }gzip, deflate, deflate-raw`,
51
52
  },
52
- maxRetryAfter: timeout,
53
53
  method: HTTP2_METHOD_GET,
54
54
  parse: true,
55
55
  redirect: requestRedirect.follow,
56
- redirected: false,
57
56
  retry: {
58
57
  attempts: 0,
59
58
  backoffStrategy: 'interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)',
@@ -68,6 +67,7 @@ const defaults = {
68
67
  'ERR_HTTP2_STREAM_ERROR',
69
68
  ],
70
69
  interval: 1e3,
70
+ maxRetryAfter: timeout,
71
71
  retryAfter: true,
72
72
  statusCodes: [
73
73
  HTTP_STATUS_TOO_MANY_REQUESTS,
package/src/cookies.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  toCamelCase,
4
4
  } from './utils.js';
5
5
 
6
- const lifetimeCap = 3456e7; // pragma: 400 days
6
+ const lifetimeCap = 3456e7; // 400 days
7
7
 
8
8
  export class Cookies extends URLSearchParams {
9
9
 
@@ -39,7 +39,7 @@ export class Cookies extends URLSearchParams {
39
39
  for (const attr of attrs) {
40
40
  if (/(?:expires|max-age)=/i.test(attr)) {
41
41
  const [key, val] = attr.toLowerCase().split('=');
42
- const ms = Number.isFinite(Number(val)) ? val * 1e3 : Date.parse(val) - Date.now();
42
+ const ms = Number.isFinite(Number.parseInt(val, 10)) ? val * 1e3 : Date.parse(val) - Date.now();
43
43
 
44
44
  ttl[toCamelCase(key)] = Math.min(ms, lifetimeCap);
45
45
  }
package/src/formdata.js CHANGED
@@ -98,11 +98,11 @@ export class FormData {
98
98
  if (!input.every((it) => Array.isArray(it))) {
99
99
  throw new TypeError(`Failed to construct '${
100
100
  this[Symbol.toStringTag]
101
- }': The provided value cannot be converted to a sequence.`);
101
+ }': The provided value cannot be converted to a sequence`);
102
102
  } else if (!input.every((it) => it.length === 2)) {
103
103
  throw new TypeError(`Failed to construct '${
104
104
  this[Symbol.toStringTag]
105
- }': Sequence initializer must only contain pair elements.`);
105
+ }': Sequence initializer must only contain pair elements`);
106
106
  }
107
107
 
108
108
  input = Array.from(input);
@@ -120,7 +120,7 @@ export class FormData {
120
120
  if (args.length < expected) {
121
121
  throw new TypeError(`Failed to execute '${ method }' on '${
122
122
  this[Symbol.toStringTag]
123
- }': ${ expected } arguments required, but only ${ args.length } present.`);
123
+ }': ${ expected } arguments required, but only ${ args.length } present`);
124
124
  }
125
125
 
126
126
  if ([
@@ -130,7 +130,7 @@ export class FormData {
130
130
  if (args.length === 3 && !this.constructor.#ensureInstance(args[1])) {
131
131
  throw new TypeError(`Failed to execute '${ method }' on '${
132
132
  this[Symbol.toStringTag]
133
- }': parameter ${ expected } is not of type 'Blob'.`);
133
+ }': parameter ${ expected } is not of type 'Blob'`);
134
134
  }
135
135
  }
136
136
 
@@ -138,7 +138,7 @@ export class FormData {
138
138
  if (args[0]?.constructor !== Function) {
139
139
  throw new TypeError(`Failed to execute '${ method }' on '${
140
140
  this[Symbol.toStringTag]
141
- }': parameter ${ expected } is not of type 'Function'.`);
141
+ }': parameter ${ expected } is not of type 'Function'`);
142
142
  }
143
143
  }
144
144
  }
package/src/mixin.js CHANGED
@@ -71,7 +71,7 @@ export const mixin = (res, { decodersOptions, digest = false, parse = false } =
71
71
  brandCheck(this, res?.constructor);
72
72
 
73
73
  if (this.bodyUsed) {
74
- throw new TypeError('Response stream already read.');
74
+ throw new TypeError('Response stream already read');
75
75
  }
76
76
 
77
77
  let body = await buffer(decode(this, this.headers[HTTP2_HEADER_CONTENT_ENCODING], { decodersOptions }));
package/src/postflight.js CHANGED
@@ -42,16 +42,20 @@ export const postflight = (req, res, options, { reject, resolve }) => {
42
42
  value: cookies !== false && Cookies.jar.has(url.origin) ? Cookies.jar.get(url.origin) : void 0,
43
43
  });
44
44
 
45
- const result = redirects(res, options);
45
+ let result;
46
46
 
47
- if (Object(result) === result) {
48
- return result.then(resolve, reject);
49
- }
47
+ try {
48
+ result = redirects(res, options);
49
+ } catch (err) {
50
+ res.emit('error', err);
50
51
 
51
- if (result) {
52
52
  return reject(mixin(res, options));
53
53
  }
54
54
 
55
+ if (Object(result) === result) {
56
+ return result.then(resolve, reject);
57
+ }
58
+
55
59
  if (res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
56
60
  return reject(mixin(res, options));
57
61
  }
package/src/redirects.js CHANGED
@@ -24,21 +24,33 @@ const {
24
24
  } = http2.constants;
25
25
 
26
26
  export const redirects = (res, options) => {
27
- const { credentials, follow, redirect, url } = options;
27
+ const {
28
+ allowDowngrade,
29
+ credentials,
30
+ follow,
31
+ redirect,
32
+ url,
33
+ } = options;
28
34
 
29
35
  if (follow && /3\d{2}/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
30
36
  if (redirect === requestRedirect.error) {
31
- return res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
37
+ throw new RequestError(`Unexpected redirect, redirect mode is set to: ${ redirect }`);
32
38
  }
33
39
 
34
40
  if (redirect === requestRedirect.follow) {
35
- const location = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
41
+ const loc = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
36
42
 
37
- if (!/^https?:/i.test(location.protocol)) {
38
- return res.emit('error', new RequestError('URL scheme must be "http" or "https".'));
43
+ if (!/^https?:/i.test(loc.protocol)) {
44
+ throw new RequestError('URL scheme must be "http" or "https"');
39
45
  }
40
46
 
41
- if (!sameOrigin(location, url)) {
47
+ if (!allowDowngrade && loc.protocol === 'http:' && url.protocol === 'https:') {
48
+ throw new RequestError(
49
+ `Protocol downgrade detected, redirect from "${ url.protocol }" to "${ loc.protocol }": ${ loc }`,
50
+ );
51
+ }
52
+
53
+ if (!sameOrigin(loc, url)) {
42
54
  if (credentials !== requestCredentials.include) {
43
55
  options.credentials = requestCredentials.omit;
44
56
  }
@@ -50,7 +62,7 @@ export const redirects = (res, options) => {
50
62
  HTTP_STATUS_PERMANENT_REDIRECT,
51
63
  HTTP_STATUS_TEMPORARY_REDIRECT,
52
64
  ].includes(res.statusCode) && isPipeStream(options.body) && !isReadable(options.body)) {
53
- return res.emit('error', new RequestError(`Unable to ${ redirect } redirect with streamable body.`));
65
+ throw new RequestError(`Unable to ${ redirect } redirect with streamable body`);
54
66
  }
55
67
 
56
68
  if (([
@@ -68,7 +80,7 @@ export const redirects = (res, options) => {
68
80
  options.follow--;
69
81
  options.redirected = true;
70
82
 
71
- return rekwest(location, options);
83
+ return rekwest(loc, options);
72
84
  }
73
85
  }
74
86
  };
package/src/retries.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import http2 from 'node:http2';
2
2
  import { isReadable } from 'node:stream';
3
- import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
3
+ import { scheduler } from 'node:timers/promises';
4
4
  import { RequestError } from './errors.js';
5
5
  import rekwest from './index.js';
6
6
  import { isPipeStream } from './utils.js';
@@ -12,14 +12,14 @@ const {
12
12
  } = http2.constants;
13
13
 
14
14
  export const retries = (err, options) => {
15
- const { body, maxRetryAfter, method, retry, url } = options;
15
+ const { body, method, retry, url } = options;
16
16
 
17
17
  if (retry?.attempts > 0) {
18
18
  if (![
19
19
  HTTP2_METHOD_GET,
20
20
  HTTP2_METHOD_HEAD,
21
21
  ].includes(method) && isPipeStream(body) && !isReadable(body)) {
22
- throw new RequestError('Request stream already read.', { cause: err });
22
+ throw new RequestError('Request stream already read', { cause: err });
23
23
  }
24
24
 
25
25
  if (retry.errorCodes?.includes(err.code) || retry.statusCodes?.includes(err.statusCode)) {
@@ -27,10 +27,10 @@ export const retries = (err, options) => {
27
27
 
28
28
  if (retry.retryAfter && err.headers?.[HTTP2_HEADER_RETRY_AFTER]) {
29
29
  interval = err.headers[HTTP2_HEADER_RETRY_AFTER];
30
- interval = Math.abs(Number(interval) * 1e3 || new Date(interval) - Date.now()) || 0;
31
- if (interval > maxRetryAfter) {
30
+ interval = Number.isFinite(Number.parseInt(interval, 10)) ? interval * 1e3 : new Date(interval) - Date.now();
31
+ if (interval > retry.maxRetryAfter) {
32
32
  throw new RequestError(
33
- `Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms.`,
33
+ `Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms`,
34
34
  { cause: err },
35
35
  );
36
36
  }
@@ -45,7 +45,7 @@ export const retries = (err, options) => {
45
45
  retry.attempts--;
46
46
  retry.interval = interval;
47
47
 
48
- return setTimeoutPromise(interval).then(() => rekwest(url, options));
48
+ return scheduler.wait(interval).then(() => rekwest(url, { ...options, params: void 0 }));
49
49
  }
50
50
  }
51
51
  };
package/src/utils.js CHANGED
@@ -62,7 +62,7 @@ export const augment = (res, headers, options) => {
62
62
 
63
63
  export const brandCheck = (val, ctor) => {
64
64
  if (!(val instanceof ctor)) {
65
- throw new TypeError('Illegal invocation.');
65
+ throw new TypeError('Illegal invocation');
66
66
  }
67
67
  };
68
68
 
@@ -137,19 +137,19 @@ export const normalize = (url, options = {}) => {
137
137
  });
138
138
  };
139
139
 
140
- export const normalizeHeaders = (headers) => {
140
+ export const normalizeHeaders = (headers = {}) => {
141
141
  const acc = {};
142
142
 
143
- for (const [key, val] of Object.entries(headers ?? {})) {
143
+ for (const [key, val] of Object.entries(headers)) {
144
144
  const name = key.toLowerCase();
145
145
 
146
146
  acc[key] = val;
147
147
 
148
148
  if (key === HTTP2_HEADER_ACCEPT_ENCODING && !isZstdSupported) {
149
- const stripped = val.replace(/\s?zstd,?/gi, '').trim();
149
+ const modified = val.replace(/\s?zstd,?/gi, '').trim();
150
150
 
151
- if (stripped) {
152
- acc[key] = stripped;
151
+ if (modified) {
152
+ acc[key] = modified;
153
153
  } else {
154
154
  Reflect.deleteProperty(acc, name);
155
155
  }
@@ -164,7 +164,7 @@ export const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname ===
164
164
  export const snoop = (client, req, options) => {
165
165
  req.once('close', () => client?.close());
166
166
  req.once('end', () => client?.close());
167
- req.once('timeout', () => req.destroy(new TimeoutError(`Timed out after ${ options.timeout } ms.`)));
167
+ req.once('timeout', () => req.destroy(new TimeoutError(`Timed out after ${ options.timeout } ms`)));
168
168
  req.once('trailers', (trailers) => {
169
169
  Reflect.defineProperty(req, 'trailers', {
170
170
  enumerable: true,
package/src/validation.js CHANGED
@@ -14,19 +14,19 @@ export const validation = (options = {}) => {
14
14
  HTTP2_METHOD_GET,
15
15
  HTTP2_METHOD_HEAD,
16
16
  ].includes(options.method)) {
17
- throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body.`);
17
+ throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body`);
18
18
  }
19
19
 
20
20
  if (!Object.values(requestCredentials).includes(options.credentials)) {
21
21
  throw new TypeError(`Failed to read the 'credentials' property from 'options': The provided value '${
22
22
  options.credentials
23
- }' is not a valid enum value.`);
23
+ }' is not a valid enum value`);
24
24
  }
25
25
 
26
26
  if (!Reflect.has(requestRedirect, options.redirect)) {
27
27
  throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${
28
28
  options.redirect
29
- }' is not a valid enum value.`);
29
+ }' is not a valid enum value`);
30
30
  }
31
31
 
32
32
  return options;