rekwest 2.3.6 → 3.0.1

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
@@ -8,12 +8,12 @@ and [http2.request](https://nodejs.org/api/http2.html#http2_clienthttp2session_r
8
8
 
9
9
  * Fetch-alike
10
10
  * Cool-beans config options (with defaults)
11
- * Automatic HTTP2 support (ALPN negotiation)
11
+ * Automatic HTTP/2 support (ALPN negotiation)
12
12
  * Automatic or opt-in body parse (with non-UTF-8 charset decoding)
13
13
  * Automatic and simplistic `Cookies` treatment (with built-in jar)
14
14
  * Automatic decompression (with opt-in body compression)
15
15
  * Built-in streamable `File` & `FormData` interfaces
16
- * Support redirects with fine-grained tune-ups
16
+ * Support redirects & retries with fine-grained tune-ups
17
17
  * Support all legit request body types (include blobs & streams)
18
18
  * Support both CJS and ESM module systems
19
19
  * Fully promise-able and pipe-able
@@ -120,54 +120,64 @@ console.log(res.body);
120
120
  extra [http2.ClientSessionOptions](https://nodejs.org/api/http2.html#http2_http2_connect_authority_options_listener)
121
121
  & [http2.ClientSessionRequestOptions](https://nodejs.org/api/http2.html#http2_clienthttp2session_request_headers_options)
122
122
  and [tls.ConnectionOptions](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback)
123
- for the HTTP2 attunes
123
+ for HTTP/2 attunes
124
124
  * `body` **{string | Array | AsyncIterator | Blob | Buffer | File | FromData | Iterator | Object | Readable |
125
- Uint8Array | URLSearchParams}** Body to send with the request
126
- * `cookies` **{boolean | Array[[key, value]] | Cookies | Object | URLSearchParams}** `Default: true` Cookies to add to
125
+ Uint8Array | URLSearchParams}** The body to send with the request
126
+ * `cookies` **{boolean | Array<[k, v]> | Cookies | Object | URLSearchParams}** `Default: true` The cookies to add to
127
127
  the request
128
- * `digest` **{boolean}** `Default: true` Read response stream, or simply add a mixin
129
- * `follow` **{number}** `Default: 20` Number of redirects to follow
130
- * `h2` **{boolean}** `Default: false` Forces use of the HTTP2 protocol
131
- * `headers` **{Object}** Headers to add to the request
132
- * `parse` **{boolean}** `Default: true` Parse response body, or simply return a buffer
133
- * `redirect` **{boolean | error | follow}** `Default: 'follow'` Controls redirect flow
134
- * `thenable` **{boolean}** `Default: false` Controls promise resolutions
128
+ * `digest` **{boolean}** `Default: true` Controls whether to read the response stream or simply add a mixin
129
+ * `follow` **{number}** `Default: 20` The number of redirects to follow
130
+ * `h2` **{boolean}** `Default: false` Forces the use of HTTP/2 protocol
131
+ * `headers` **{Object}** The headers to add to the request
132
+ * `maxRetryAfter` **{number}** The upper limit of `retry-after` header. If unset, it will use `timeout` value
133
+ * `parse` **{boolean}** `Default: true` Controls whether to parse response body or simply return a buffer
134
+ * `redirect` **{error | follow | manual}** `Default: follow` Controls the redirect flows
135
+ * `retry` **{Object}** Represents the retry options
136
+ * `attempts` **{number}** `Default: 0` The number of retry attempts
137
+ * `backoffStrategy` **{string}** `Default: interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)`
138
+ The backoff strategy algorithm that increases logarithmically. To fixate set value to `interval * 1`
139
+ * `interval` **{number}** `Default: 1e3` The initial retry interval
140
+ * `retryAfter` **{boolean}** `Default: true` Controls `retry-after` header receptiveness
141
+ * `statusCodes` **{number[]}** `Default: [429, 503]` The list of status codes to retry on
142
+ * `thenable` **{boolean}** `Default: false` Controls the promise resolutions
143
+ * `timeout` **{number}** `Default: 3e5` The number of milliseconds a request can take before termination
135
144
  * **Returns:** Promise that resolves to
136
145
  extended [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
137
146
  or [http2.ClientHttp2Stream](https://nodejs.org/api/http2.html#http2_class_clienthttp2stream) which is respectively
138
147
  readable and duplex streams
139
148
  * if `degist: true` & `parse: true`
140
- * `body` **{string | Array | Buffer | Object}** Body based on its content type
149
+ * `body` **{string | Array | Buffer | Object}** The body based on its content type
141
150
  * if `degist: false`
142
151
  * `arrayBuffer` **{AsyncFunction}** Reads the response and returns **ArrayBuffer**
143
152
  * `blob` **{AsyncFunction}** Reads the response and returns **Blob**
144
153
  * `body` **{AsyncFunction}** Reads the response and returns **Buffer** if `parse: false`
145
154
  * `json` **{AsyncFunction}** Reads the response and returns **Object**
146
155
  * `text` **{AsyncFunction}** Reads the response and returns **String**
147
- * `bodyUsed` **{boolean}** Whether the response were read or not
148
- * `cookies` **{undefined | Cookies}** Cookies sent and received with the response
149
- * `headers` **{Object}** Headers received with the response
156
+ * `bodyUsed` **{boolean}** Indicates whether the response were read or not
157
+ * `cookies` **{undefined | Cookies}** The cookies sent and received with the response
158
+ * `headers` **{Object}** The headers received with the response
150
159
  * `httpVersion` **{string}** Indicates protocol version negotiated with the server
151
160
  * `ok` **{boolean}** Indicates if the response was successful (statusCode: **200-299**)
152
161
  * `redirected` **{boolean}** Indicates if the response is the result of a redirect
153
162
  * `statusCode` **{number}** Indicates the status code of the response
154
- * `trailers` **{undefined | Object}** Trailer headers received with the response
163
+ * `trailers` **{undefined | Object}** The trailer headers received with the response
155
164
 
156
165
  ---
157
166
 
158
167
  #### `rekwest.defaults`
159
168
 
160
- Object to fill with default [options](#rekwesturl-options)
169
+ The object to fulfill with default [options](#rekwesturl-options)
161
170
 
162
171
  ---
163
172
 
164
173
  #### `rekwest.stream(url[, options])`
165
174
 
166
- Method with limited functionality to use with streams and pipes
175
+ The method with limited functionality to use with streams and/or pipes
167
176
 
168
177
  * No automata
169
178
  * No redirects
170
- * Pass `h2: true` in options to use the HTTP2 protocol
179
+ * Pass `h2: true` in options to use HTTP/2 protocol
180
+ * Or use `ackn({ url: URL })` method in advance to probe the available protocols
171
181
 
172
182
  ---
173
183
 
package/dist/errors.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.RequestError = void 0;
4
+ exports.TimeoutError = exports.RequestError = void 0;
5
5
 
6
6
  class RequestError extends Error {
7
7
  get [Symbol.toStringTag]() {
@@ -12,11 +12,15 @@ class RequestError extends Error {
12
12
  return this[Symbol.toStringTag];
13
13
  }
14
14
 
15
- constructor(message) {
16
- super(message);
15
+ constructor(...args) {
16
+ super(...args);
17
17
  Error.captureStackTrace(this, this.constructor);
18
18
  }
19
19
 
20
20
  }
21
21
 
22
- exports.RequestError = RequestError;
22
+ exports.RequestError = RequestError;
23
+
24
+ class TimeoutError extends RequestError {}
25
+
26
+ exports.TimeoutError = TimeoutError;
package/dist/helpers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.premix = exports.preflight = exports.merge = exports.dispatch = exports.decompress = exports.compress = void 0;
4
+ exports.redirects = exports.preflight = exports.mixin = exports.merge = exports.dispatch = exports.decompress = exports.compress = exports.affix = exports.admix = void 0;
5
5
  exports.tap = tap;
6
6
  exports.transform = void 0;
7
7
 
@@ -17,6 +17,8 @@ var _zlib = _interopRequireDefault(require("zlib"));
17
17
 
18
18
  var _cookies = require("./cookies.js");
19
19
 
20
+ var _errors = require("./errors.js");
21
+
20
22
  var _file = require("./file.js");
21
23
 
22
24
  var _formdata = require("./formdata.js");
@@ -36,6 +38,7 @@ const {
36
38
  HTTP2_HEADER_METHOD,
37
39
  HTTP2_HEADER_PATH,
38
40
  HTTP2_HEADER_SCHEME,
41
+ HTTP2_HEADER_STATUS,
39
42
  HTTP2_METHOD_GET,
40
43
  HTTP2_METHOD_HEAD
41
44
  } = _http.default.constants;
@@ -46,6 +49,51 @@ const gunzip = (0, _util.promisify)(_zlib.default.gunzip);
46
49
  const deflate = (0, _util.promisify)(_zlib.default.deflate);
47
50
  const inflate = (0, _util.promisify)(_zlib.default.inflate);
48
51
 
52
+ const admix = (res, headers, options) => {
53
+ const {
54
+ h2
55
+ } = options;
56
+
57
+ if (h2) {
58
+ Reflect.defineProperty(res, 'headers', {
59
+ enumerable: true,
60
+ value: headers
61
+ });
62
+ Reflect.defineProperty(res, 'httpVersion', {
63
+ enumerable: true,
64
+ value: `${h2 + 1}.0`
65
+ });
66
+ Reflect.defineProperty(res, 'statusCode', {
67
+ enumerable: true,
68
+ value: headers[HTTP2_HEADER_STATUS]
69
+ });
70
+ }
71
+
72
+ Reflect.defineProperty(res, 'ok', {
73
+ enumerable: true,
74
+ value: /^2\d{2}$/.test(res.statusCode)
75
+ });
76
+ Reflect.defineProperty(res, 'redirected', {
77
+ enumerable: true,
78
+ value: !!options.redirected
79
+ });
80
+ };
81
+
82
+ exports.admix = admix;
83
+
84
+ const affix = (client, req, options) => {
85
+ req.once('end', () => client?.close());
86
+ req.once('timeout', () => req.destroy(new _errors.TimeoutError(`Timed out after ${options.timeout} ms.`)));
87
+ req.once('trailers', trailers => {
88
+ Reflect.defineProperty(req, 'trailers', {
89
+ enumerable: true,
90
+ value: trailers
91
+ });
92
+ });
93
+ };
94
+
95
+ exports.affix = affix;
96
+
49
97
  const compress = (buf, encoding, {
50
98
  async = false
51
99
  } = {}) => {
@@ -127,67 +175,7 @@ const merge = (target = {}, ...rest) => {
127
175
 
128
176
  exports.merge = merge;
129
177
 
130
- const preflight = options => {
131
- const url = options.url = new URL(options.url);
132
- const {
133
- cookies,
134
- h2 = false,
135
- method = HTTP2_METHOD_GET,
136
- headers,
137
- redirected
138
- } = options;
139
-
140
- if (h2) {
141
- options.endStream = [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(method);
142
- }
143
-
144
- if (cookies !== false) {
145
- let cookie = _cookies.Cookies.jar.get(url.origin);
146
-
147
- if (cookies === Object(cookies) && !redirected) {
148
- if (cookie) {
149
- new _cookies.Cookies(cookies).forEach(function (val, key) {
150
- this.set(key, val);
151
- }, cookie);
152
- } else {
153
- cookie = new _cookies.Cookies(cookies);
154
-
155
- _cookies.Cookies.jar.set(url.origin, cookie);
156
- }
157
- }
158
-
159
- options.headers = { ...(cookie && {
160
- [HTTP2_HEADER_COOKIE]: cookie
161
- }),
162
- ...headers
163
- };
164
- }
165
-
166
- options.digest ??= true;
167
- options.follow ??= 20;
168
- options.h2 ??= h2;
169
- options.headers = {
170
- [HTTP2_HEADER_ACCEPT]: `${_mediatypes.APPLICATION_JSON}, ${_mediatypes.TEXT_PLAIN}, ${_mediatypes.WILDCARD}`,
171
- [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, gzip, identity',
172
- ...Object.entries(options.headers ?? {}).reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
173
- ...(h2 && {
174
- [HTTP2_HEADER_AUTHORITY]: url.host,
175
- [HTTP2_HEADER_METHOD]: method,
176
- [HTTP2_HEADER_PATH]: `${url.pathname}${url.search}`,
177
- [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, '')
178
- })
179
- };
180
- options.method ??= method;
181
- options.parse ??= true;
182
- options.redirect ??= 'follow';
183
- options.redirected ??= false;
184
- options.thenable ??= false;
185
- return options;
186
- };
187
-
188
- exports.preflight = preflight;
189
-
190
- const premix = (res, {
178
+ const mixin = (res, {
191
179
  digest = false,
192
180
  parse = false
193
181
  } = {}) => {
@@ -230,7 +218,7 @@ const premix = (res, {
230
218
  enumerable: true,
231
219
  value: async function () {
232
220
  if (this.bodyUsed) {
233
- throw new TypeError('Response stream already read');
221
+ throw new TypeError('Response stream already read.');
234
222
  }
235
223
 
236
224
  let spool = [];
@@ -275,7 +263,79 @@ const premix = (res, {
275
263
  });
276
264
  };
277
265
 
278
- exports.premix = premix;
266
+ exports.mixin = mixin;
267
+
268
+ const preflight = options => {
269
+ const url = options.url = new URL(options.url);
270
+ const {
271
+ cookies,
272
+ h2 = false,
273
+ method = HTTP2_METHOD_GET,
274
+ headers,
275
+ redirected
276
+ } = options;
277
+
278
+ if (h2) {
279
+ options.endStream = [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(method);
280
+ }
281
+
282
+ if (cookies !== false) {
283
+ let cookie = _cookies.Cookies.jar.get(url.origin);
284
+
285
+ if (cookies === Object(cookies) && !redirected) {
286
+ if (cookie) {
287
+ new _cookies.Cookies(cookies).forEach(function (val, key) {
288
+ this.set(key, val);
289
+ }, cookie);
290
+ } else {
291
+ cookie = new _cookies.Cookies(cookies);
292
+
293
+ _cookies.Cookies.jar.set(url.origin, cookie);
294
+ }
295
+ }
296
+
297
+ options.headers = { ...(cookie && {
298
+ [HTTP2_HEADER_COOKIE]: cookie
299
+ }),
300
+ ...headers
301
+ };
302
+ }
303
+
304
+ options.digest ??= true;
305
+ options.follow ??= 20;
306
+ options.h2 ??= h2;
307
+ options.headers = {
308
+ [HTTP2_HEADER_ACCEPT]: `${_mediatypes.APPLICATION_JSON}, ${_mediatypes.TEXT_PLAIN}, ${_mediatypes.WILDCARD}`,
309
+ [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, gzip, identity',
310
+ ...Object.entries(options.headers ?? {}).reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
311
+ ...(h2 && {
312
+ [HTTP2_HEADER_AUTHORITY]: url.host,
313
+ [HTTP2_HEADER_METHOD]: method,
314
+ [HTTP2_HEADER_PATH]: `${url.pathname}${url.search}`,
315
+ [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, '')
316
+ })
317
+ };
318
+ options.method ??= method;
319
+ options.parse ??= true;
320
+ options.redirect ??= redirects.follow;
321
+
322
+ if (!Object.values(redirects).includes(options.redirect)) {
323
+ options.createConnection?.().destroy();
324
+ throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${options.redirect}' is not a valid enum value.`);
325
+ }
326
+
327
+ options.redirected ??= false;
328
+ options.thenable ??= false;
329
+ return options;
330
+ };
331
+
332
+ exports.preflight = preflight;
333
+ const redirects = {
334
+ error: 'error',
335
+ follow: 'follow',
336
+ manual: 'manual'
337
+ };
338
+ exports.redirects = redirects;
279
339
 
280
340
  async function* tap(value) {
281
341
  if (Reflect.has(value, Symbol.asyncIterator)) {
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ exports.constants = _http2.constants;
15
15
 
16
16
  var _https = _interopRequireDefault(require("https"));
17
17
 
18
+ var _promises = require("timers/promises");
19
+
18
20
  var _ackn = require("./ackn.js");
19
21
 
20
22
  Object.keys(_ackn).forEach(function (key) {
@@ -77,29 +79,54 @@ const {
77
79
  HTTP2_HEADER_CONTENT_LENGTH,
78
80
  HTTP2_HEADER_CONTENT_TYPE,
79
81
  HTTP2_HEADER_LOCATION,
82
+ HTTP2_HEADER_RETRY_AFTER,
80
83
  HTTP2_HEADER_SET_COOKIE,
81
- HTTP2_HEADER_STATUS,
82
84
  HTTP2_METHOD_GET,
83
85
  HTTP2_METHOD_HEAD,
84
86
  HTTP_STATUS_BAD_REQUEST,
85
- HTTP_STATUS_SEE_OTHER
87
+ HTTP_STATUS_MOVED_PERMANENTLY,
88
+ HTTP_STATUS_SEE_OTHER,
89
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
90
+ HTTP_STATUS_TOO_MANY_REQUESTS
86
91
  } = _http2.default.constants;
92
+ const maxRetryAfter = Symbol('maxRetryAfter');
93
+
94
+ const maxRetryAfterError = (interval, options) => new _errors.RequestError(`Maximum '${HTTP2_HEADER_RETRY_AFTER}' limit exceeded: ${interval} ms.`, options);
95
+
96
+ let defaults = {
97
+ follow: 20,
98
+
99
+ get maxRetryAfter() {
100
+ return this[maxRetryAfter] ?? this.timeout;
101
+ },
102
+
103
+ set maxRetryAfter(value) {
104
+ this[maxRetryAfter] = value;
105
+ },
106
+
107
+ method: HTTP2_METHOD_GET,
108
+ retry: {
109
+ attempts: 0,
110
+ backoffStrategy: 'interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)',
111
+ interval: 1e3,
112
+ retryAfter: true,
113
+ statusCodes: [HTTP_STATUS_TOO_MANY_REQUESTS, HTTP_STATUS_SERVICE_UNAVAILABLE]
114
+ },
115
+ timeout: 3e5
116
+ };
87
117
 
88
118
  async function rekwest(url, options = {}) {
89
119
  url = options.url = new URL(url);
90
120
 
91
121
  if (!options.redirected) {
92
- options = (0, _helpers.merge)(rekwest.defaults, {
93
- follow: 20,
94
- method: HTTP2_METHOD_GET
95
- }, options);
122
+ options = (0, _helpers.merge)(rekwest.defaults, options);
96
123
  }
97
124
 
98
125
  if (options.body && [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(options.method)) {
99
- throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body`);
126
+ throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body.`);
100
127
  }
101
128
 
102
- if (!options.follow) {
129
+ if (options.follow === 0) {
103
130
  throw new _errors.RequestError(`Maximum redirect reached at: ${url.href}`);
104
131
  }
105
132
 
@@ -139,26 +166,22 @@ async function rekwest(url, options = {}) {
139
166
  req = request(url, options);
140
167
  }
141
168
 
142
- req.on('response', res => {
169
+ (0, _helpers.affix)(client, req, options);
170
+ req.once('error', reject);
171
+ req.once('frameError', reject);
172
+ req.once('goaway', reject);
173
+ req.once('response', res => {
174
+ let headers;
175
+
143
176
  if (h2) {
144
- const headers = res;
177
+ headers = res;
145
178
  res = req;
146
- Reflect.defineProperty(res, 'headers', {
147
- enumerable: true,
148
- value: headers
149
- });
150
- Reflect.defineProperty(res, 'httpVersion', {
151
- enumerable: true,
152
- value: `${h2 + 1}.0`
153
- });
154
- Reflect.defineProperty(res, 'statusCode', {
155
- enumerable: true,
156
- value: headers[HTTP2_HEADER_STATUS]
157
- });
158
179
  } else {
159
- res.on('error', reject);
180
+ res.once('error', reject);
160
181
  }
161
182
 
183
+ (0, _helpers.admix)(res, headers, options);
184
+
162
185
  if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
163
186
  if (_cookies.Cookies.jar.has(url.origin)) {
164
187
  new _cookies.Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]).forEach(function (val, key) {
@@ -175,15 +198,15 @@ async function rekwest(url, options = {}) {
175
198
  });
176
199
 
177
200
  if (follow && /^3\d{2}$/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
178
- if (redirect === 'error') {
179
- res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'`));
201
+ if (redirect === _helpers.redirects.error) {
202
+ res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'.`));
180
203
  }
181
204
 
182
- if (redirect === 'follow') {
205
+ if (redirect === _helpers.redirects.follow) {
183
206
  options.url = new URL(res.headers[HTTP2_HEADER_LOCATION], url).href;
184
207
 
185
208
  if (res.statusCode !== HTTP_STATUS_SEE_OTHER && body === Object(body) && body.pipe?.constructor === Function) {
186
- res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with body as readable stream`));
209
+ res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with body as readable stream.`));
187
210
  }
188
211
 
189
212
  options.follow--;
@@ -195,37 +218,29 @@ async function rekwest(url, options = {}) {
195
218
  }
196
219
 
197
220
  Reflect.set(options, 'redirected', true);
221
+
222
+ if (res.statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
223
+ let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
224
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
225
+
226
+ if (interval > options.maxRetryAfter) {
227
+ res.emit('error', maxRetryAfterError(interval, {
228
+ cause: (0, _helpers.mixin)(res, options)
229
+ }));
230
+ }
231
+
232
+ return (0, _promises.setTimeout)(interval).then(() => rekwest(options.url, options).then(resolve, reject));
233
+ }
234
+
198
235
  return rekwest(options.url, options).then(resolve, reject);
199
236
  }
200
237
  }
201
238
 
202
- Reflect.defineProperty(res, 'ok', {
203
- enumerable: true,
204
- value: /^2\d{2}$/.test(res.statusCode)
205
- });
206
- Reflect.defineProperty(res, 'redirected', {
207
- enumerable: true,
208
- value: options.redirected
209
- });
210
-
211
239
  if (res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
212
- return reject((0, _helpers.premix)(res, options));
240
+ return reject((0, _helpers.mixin)(res, options));
213
241
  }
214
242
 
215
- resolve((0, _helpers.premix)(res, options));
216
- });
217
- req.on('end', () => {
218
- client?.close();
219
- });
220
- req.on('error', reject);
221
- req.on('frameError', reject);
222
- req.on('goaway', reject);
223
- req.on('timeout', req.destroy);
224
- req.on('trailers', trailers => {
225
- Reflect.defineProperty(req, 'trailers', {
226
- enumerable: true,
227
- value: trailers
228
- });
243
+ resolve((0, _helpers.mixin)(res, options));
229
244
  });
230
245
  (0, _helpers.dispatch)(req, { ...options,
231
246
  body
@@ -241,6 +256,29 @@ async function rekwest(url, options = {}) {
241
256
 
242
257
  return res;
243
258
  } catch (ex) {
259
+ if (options.retry?.attempts && options.retry?.statusCodes.includes(ex.statusCode)) {
260
+ let {
261
+ interval
262
+ } = options.retry;
263
+
264
+ if (options.retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
265
+ interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
266
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
267
+
268
+ if (interval > options.maxRetryAfter) {
269
+ throw maxRetryAfterError(interval, {
270
+ cause: ex
271
+ });
272
+ }
273
+ } else {
274
+ interval = new Function('interval', `return Math.ceil(${options.retry.backoffStrategy});`)(interval);
275
+ }
276
+
277
+ options.retry.attempts--;
278
+ options.retry.interval = interval;
279
+ return (0, _promises.setTimeout)(interval).then(() => rekwest(url, options));
280
+ }
281
+
244
282
  if (digest && !redirected && ex.body) {
245
283
  ex.body = await ex.body();
246
284
  }
@@ -262,20 +300,11 @@ Reflect.defineProperty(rekwest, 'stream', {
262
300
  headers: {
263
301
  [HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_OCTET_STREAM
264
302
  }
265
- }, options)
303
+ }, options),
304
+ redirect: _helpers.redirects.manual
266
305
  });
267
-
268
- if (options.h2) {
269
- const client = _http2.default.connect(url.origin, options);
270
-
271
- const req = client.request(options.headers, options);
272
- req.on('end', () => {
273
- client.close();
274
- });
275
- return req;
276
- }
277
-
278
306
  const {
307
+ h2,
279
308
  url: {
280
309
  protocol
281
310
  }
@@ -283,11 +312,38 @@ Reflect.defineProperty(rekwest, 'stream', {
283
312
  const {
284
313
  request
285
314
  } = protocol === 'http:' ? _http.default : _https.default;
286
- return request(options.url, options);
315
+ let client, req;
316
+
317
+ if (h2) {
318
+ client = _http2.default.connect(url.origin, options);
319
+ req = client.request(options.headers, options);
320
+ } else {
321
+ req = request(options.url, options);
322
+ }
323
+
324
+ (0, _helpers.affix)(client, req, options);
325
+ req.once('response', res => {
326
+ let headers;
327
+
328
+ if (h2) {
329
+ headers = res;
330
+ res = req;
331
+ }
332
+
333
+ (0, _helpers.admix)(res, headers, options);
334
+ });
335
+ return req;
287
336
  }
288
337
  });
289
338
  Reflect.defineProperty(rekwest, 'defaults', {
290
339
  enumerable: true,
291
- value: Object.create(null),
292
- writable: true
340
+
341
+ get() {
342
+ return defaults;
343
+ },
344
+
345
+ set(value) {
346
+ defaults = (0, _helpers.merge)(defaults, value);
347
+ }
348
+
293
349
  });
package/package.json CHANGED
@@ -8,14 +8,14 @@
8
8
  "url": "https://github.com/bricss/rekwest/issues"
9
9
  },
10
10
  "devDependencies": {
11
- "@babel/cli": "^7.16.8",
12
- "@babel/core": "^7.16.12",
13
- "@babel/eslint-parser": "^7.16.5",
11
+ "@babel/cli": "^7.17.6",
12
+ "@babel/core": "^7.17.5",
13
+ "@babel/eslint-parser": "^7.17.0",
14
14
  "@babel/preset-env": "^7.16.11",
15
15
  "c8": "^7.11.0",
16
- "eslint": "^8.7.0",
17
- "eslint-config-ultra-refined": "^2.3.0",
18
- "mocha": "^9.2.0"
16
+ "eslint": "^8.10.0",
17
+ "eslint-config-ultra-refined": "^2.4.1",
18
+ "mocha": "^9.2.1"
19
19
  },
20
20
  "description": "The robust request library that humanity deserves 🌐",
21
21
  "engines": {
@@ -56,8 +56,9 @@
56
56
  "lint": "eslint . --ext .cjs,.js,.mjs",
57
57
  "prepack": "npm run build && sh pony.sh",
58
58
  "pretest": "rm -rf coverage && npm run cert:gen",
59
- "test": "mocha --exit --recursive",
59
+ "test": "mocha",
60
+ "test:bail": "mocha --bail",
60
61
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
61
62
  },
62
- "version": "2.3.6"
63
+ "version": "3.0.1"
63
64
  }