rekwest 2.4.0 → 3.1.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
@@ -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,63 @@ 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
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
123
+ for HTTP/2 attunes
124
+ * `body` **{string | Array | ArrayBuffer | ArrayBufferView | AsyncIterator | Blob | Buffer | DataView | File |
125
+ FromData | Iterator | Object | Readable | SharedArrayBuffer | 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` **{error | follow | manual}** `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
171
180
  * Or use `ackn({ url: URL })` method in advance to probe the available protocols
172
181
 
173
182
  ---
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.redirects = 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
  } = {}) => {
@@ -78,10 +126,6 @@ const dispatch = (req, {
78
126
  body,
79
127
  headers
80
128
  }) => {
81
- if (_util.types.isUint8Array(body)) {
82
- return req.end(body);
83
- }
84
-
85
129
  if (body === Object(body) && !Buffer.isBuffer(body)) {
86
130
  if (body.pipe?.constructor !== Function && (Reflect.has(body, Symbol.asyncIterator) || Reflect.has(body, Symbol.iterator))) {
87
131
  body = _stream.Readable.from(body);
@@ -127,73 +171,7 @@ const merge = (target = {}, ...rest) => {
127
171
 
128
172
  exports.merge = merge;
129
173
 
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 ??= redirects.follow;
183
-
184
- if (!Object.values(redirects).includes(options.redirect)) {
185
- options.createConnection?.().destroy();
186
- throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${options.redirect}' is not a valid enum value.`);
187
- }
188
-
189
- options.redirected ??= false;
190
- options.thenable ??= false;
191
- return options;
192
- };
193
-
194
- exports.preflight = preflight;
195
-
196
- const premix = (res, {
174
+ const mixin = (res, {
197
175
  digest = false,
198
176
  parse = false
199
177
  } = {}) => {
@@ -281,7 +259,73 @@ const premix = (res, {
281
259
  });
282
260
  };
283
261
 
284
- exports.premix = premix;
262
+ exports.mixin = mixin;
263
+
264
+ const preflight = options => {
265
+ const url = options.url = new URL(options.url);
266
+ const {
267
+ cookies,
268
+ h2 = false,
269
+ method = HTTP2_METHOD_GET,
270
+ headers,
271
+ redirected
272
+ } = options;
273
+
274
+ if (h2) {
275
+ options.endStream = [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(method);
276
+ }
277
+
278
+ if (cookies !== false) {
279
+ let cookie = _cookies.Cookies.jar.get(url.origin);
280
+
281
+ if (cookies === Object(cookies) && !redirected) {
282
+ if (cookie) {
283
+ new _cookies.Cookies(cookies).forEach(function (val, key) {
284
+ this.set(key, val);
285
+ }, cookie);
286
+ } else {
287
+ cookie = new _cookies.Cookies(cookies);
288
+
289
+ _cookies.Cookies.jar.set(url.origin, cookie);
290
+ }
291
+ }
292
+
293
+ options.headers = { ...(cookie && {
294
+ [HTTP2_HEADER_COOKIE]: cookie
295
+ }),
296
+ ...headers
297
+ };
298
+ }
299
+
300
+ options.digest ??= true;
301
+ options.follow ??= 20;
302
+ options.h2 ??= h2;
303
+ options.headers = {
304
+ [HTTP2_HEADER_ACCEPT]: `${_mediatypes.APPLICATION_JSON}, ${_mediatypes.TEXT_PLAIN}, ${_mediatypes.WILDCARD}`,
305
+ [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, gzip, identity',
306
+ ...Object.entries(options.headers ?? {}).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
+ options.method ??= method;
315
+ options.parse ??= true;
316
+ options.redirect ??= redirects.follow;
317
+
318
+ if (!Object.values(redirects).includes(options.redirect)) {
319
+ options.createConnection?.().destroy();
320
+ throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${options.redirect}' is not a valid enum value.`);
321
+ }
322
+
323
+ options.redirected ??= false;
324
+ options.thenable ??= false;
325
+ return options;
326
+ };
327
+
328
+ exports.preflight = preflight;
285
329
  const redirects = {
286
330
  error: 'error',
287
331
  follow: 'follow',
@@ -302,6 +346,12 @@ async function* tap(value) {
302
346
  const transform = (body, options) => {
303
347
  let headers = {};
304
348
 
349
+ if (_util.types.isAnyArrayBuffer(body) && !Buffer.isBuffer(body)) {
350
+ body = Buffer.from(body);
351
+ } else if (_util.types.isArrayBufferView(body) && !Buffer.isBuffer(body)) {
352
+ body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
353
+ }
354
+
305
355
  if (_file.File.alike(body)) {
306
356
  headers = {
307
357
  [HTTP2_HEADER_CONTENT_LENGTH]: body.size,
@@ -326,7 +376,7 @@ const transform = (body, options) => {
326
376
  body = JSON.stringify(body);
327
377
  }
328
378
 
329
- if (_util.types.isUint8Array(body) || Buffer.isBuffer(body) || body !== Object(body)) {
379
+ if (Buffer.isBuffer(body) || body !== Object(body)) {
330
380
  if (options.headers[HTTP2_HEADER_CONTENT_ENCODING]) {
331
381
  body = compress(body, options.headers[HTTP2_HEADER_CONTENT_ENCODING]);
332
382
  }
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
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) {
@@ -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,34 @@ async function rekwest(url, options = {}) {
241
256
 
242
257
  return res;
243
258
  } catch (ex) {
259
+ const {
260
+ maxRetryAfter,
261
+ retry
262
+ } = options;
263
+
264
+ if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
265
+ let {
266
+ interval
267
+ } = retry;
268
+
269
+ if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
270
+ interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
271
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
272
+
273
+ if (interval > maxRetryAfter) {
274
+ throw maxRetryAfterError(interval, {
275
+ cause: ex
276
+ });
277
+ }
278
+ } else {
279
+ interval = new Function('interval', `return Math.ceil(${retry.backoffStrategy});`)(interval);
280
+ }
281
+
282
+ retry.attempts--;
283
+ retry.interval = interval;
284
+ return (0, _promises.setTimeout)(interval).then(() => rekwest(url, options));
285
+ }
286
+
244
287
  if (digest && !redirected && ex.body) {
245
288
  ex.body = await ex.body();
246
289
  }
@@ -265,18 +308,8 @@ Reflect.defineProperty(rekwest, 'stream', {
265
308
  }, options),
266
309
  redirect: _helpers.redirects.manual
267
310
  });
268
-
269
- if (options.h2) {
270
- const client = _http2.default.connect(url.origin, options);
271
-
272
- const req = client.request(options.headers, options);
273
- req.on('end', () => {
274
- client.close();
275
- });
276
- return req;
277
- }
278
-
279
311
  const {
312
+ h2,
280
313
  url: {
281
314
  protocol
282
315
  }
@@ -284,11 +317,38 @@ Reflect.defineProperty(rekwest, 'stream', {
284
317
  const {
285
318
  request
286
319
  } = protocol === 'http:' ? _http.default : _https.default;
287
- return request(options.url, options);
320
+ let client, req;
321
+
322
+ if (h2) {
323
+ client = _http2.default.connect(url.origin, options);
324
+ req = client.request(options.headers, options);
325
+ } else {
326
+ req = request(options.url, options);
327
+ }
328
+
329
+ (0, _helpers.affix)(client, req, options);
330
+ req.once('response', res => {
331
+ let headers;
332
+
333
+ if (h2) {
334
+ headers = res;
335
+ res = req;
336
+ }
337
+
338
+ (0, _helpers.admix)(res, headers, options);
339
+ });
340
+ return req;
288
341
  }
289
342
  });
290
343
  Reflect.defineProperty(rekwest, 'defaults', {
291
344
  enumerable: true,
292
- value: Object.create(null),
293
- writable: true
345
+
346
+ get() {
347
+ return defaults;
348
+ },
349
+
350
+ set(value) {
351
+ defaults = (0, _helpers.merge)(defaults, value);
352
+ }
353
+
294
354
  });
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.8.0",
17
- "eslint-config-ultra-refined": "^2.4.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": {
@@ -60,5 +60,5 @@
60
60
  "test:bail": "mocha --bail",
61
61
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
62
62
  },
63
- "version": "2.4.0"
63
+ "version": "3.1.0"
64
64
  }