rekwest 2.3.5 → 3.0.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,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 just 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,15 +1,13 @@
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
 
8
8
  var _buffer = require("buffer");
9
9
 
10
- var _http = require("http");
11
-
12
- var _http2 = _interopRequireDefault(require("http2"));
10
+ var _http = _interopRequireDefault(require("http2"));
13
11
 
14
12
  var _stream = require("stream");
15
13
 
@@ -19,6 +17,8 @@ var _zlib = _interopRequireDefault(require("zlib"));
19
17
 
20
18
  var _cookies = require("./cookies.js");
21
19
 
20
+ var _errors = require("./errors.js");
21
+
22
22
  var _file = require("./file.js");
23
23
 
24
24
  var _formdata = require("./formdata.js");
@@ -38,9 +38,10 @@ const {
38
38
  HTTP2_HEADER_METHOD,
39
39
  HTTP2_HEADER_PATH,
40
40
  HTTP2_HEADER_SCHEME,
41
+ HTTP2_HEADER_STATUS,
41
42
  HTTP2_METHOD_GET,
42
43
  HTTP2_METHOD_HEAD
43
- } = _http2.default.constants;
44
+ } = _http.default.constants;
44
45
  const brotliCompress = (0, _util.promisify)(_zlib.default.brotliCompress);
45
46
  const brotliDecompress = (0, _util.promisify)(_zlib.default.brotliDecompress);
46
47
  const gzip = (0, _util.promisify)(_zlib.default.gzip);
@@ -48,6 +49,51 @@ const gunzip = (0, _util.promisify)(_zlib.default.gunzip);
48
49
  const deflate = (0, _util.promisify)(_zlib.default.deflate);
49
50
  const inflate = (0, _util.promisify)(_zlib.default.inflate);
50
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
+
51
97
  const compress = (buf, encoding, {
52
98
  async = false
53
99
  } = {}) => {
@@ -129,69 +175,7 @@ const merge = (target = {}, ...rest) => {
129
175
 
130
176
  exports.merge = merge;
131
177
 
132
- const preflight = options => {
133
- const url = options.url = new URL(options.url);
134
- const {
135
- cookies,
136
- h2 = false,
137
- method = HTTP2_METHOD_GET,
138
- headers,
139
- redirected
140
- } = options;
141
-
142
- if (!h2) {
143
- options.agent ??= url.protocol === 'http:' ? _http.globalAgent : void 0;
144
- } else {
145
- options.endStream = [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(method);
146
- }
147
-
148
- if (cookies !== false) {
149
- let cookie = _cookies.Cookies.jar.get(url.origin);
150
-
151
- if (cookies === Object(cookies) && !redirected) {
152
- if (cookie) {
153
- new _cookies.Cookies(cookies).forEach(function (val, key) {
154
- this.set(key, val);
155
- }, cookie);
156
- } else {
157
- cookie = new _cookies.Cookies(cookies);
158
-
159
- _cookies.Cookies.jar.set(url.origin, cookie);
160
- }
161
- }
162
-
163
- options.headers = { ...(cookie && {
164
- [HTTP2_HEADER_COOKIE]: cookie
165
- }),
166
- ...headers
167
- };
168
- }
169
-
170
- options.digest ??= true;
171
- options.follow ??= 20;
172
- options.h2 ??= h2;
173
- options.headers = {
174
- [HTTP2_HEADER_ACCEPT]: `${_mediatypes.APPLICATION_JSON}, ${_mediatypes.TEXT_PLAIN}, ${_mediatypes.WILDCARD}`,
175
- [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, gzip, identity',
176
- ...Object.entries(options.headers ?? {}).reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
177
- ...(h2 && {
178
- [HTTP2_HEADER_AUTHORITY]: url.host,
179
- [HTTP2_HEADER_METHOD]: method,
180
- [HTTP2_HEADER_PATH]: `${url.pathname}${url.search}`,
181
- [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, '')
182
- })
183
- };
184
- options.method ??= method;
185
- options.parse ??= true;
186
- options.redirect ??= 'follow';
187
- options.redirected ??= false;
188
- options.thenable ??= false;
189
- return options;
190
- };
191
-
192
- exports.preflight = preflight;
193
-
194
- const premix = (res, {
178
+ const mixin = (res, {
195
179
  digest = false,
196
180
  parse = false
197
181
  } = {}) => {
@@ -234,7 +218,7 @@ const premix = (res, {
234
218
  enumerable: true,
235
219
  value: async function () {
236
220
  if (this.bodyUsed) {
237
- throw new TypeError('Response stream already read');
221
+ throw new TypeError('Response stream already read.');
238
222
  }
239
223
 
240
224
  let spool = [];
@@ -279,7 +263,79 @@ const premix = (res, {
279
263
  });
280
264
  };
281
265
 
282
- 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;
283
339
 
284
340
  async function* tap(value) {
285
341
  if (Reflect.has(value, Symbol.asyncIterator)) {
package/dist/index.js CHANGED
@@ -7,11 +7,15 @@ var _exportNames = {
7
7
  exports.constants = void 0;
8
8
  exports.default = rekwest;
9
9
 
10
- var _http = _interopRequireDefault(require("http2"));
10
+ var _http = _interopRequireDefault(require("http"));
11
11
 
12
- exports.constants = _http.constants;
12
+ var _http2 = _interopRequireDefault(require("http2"));
13
13
 
14
- var _https = require("https");
14
+ exports.constants = _http2.constants;
15
+
16
+ var _https = _interopRequireDefault(require("https"));
17
+
18
+ var _promises = require("timers/promises");
15
19
 
16
20
  var _ackn = require("./ackn.js");
17
21
 
@@ -75,29 +79,55 @@ const {
75
79
  HTTP2_HEADER_CONTENT_LENGTH,
76
80
  HTTP2_HEADER_CONTENT_TYPE,
77
81
  HTTP2_HEADER_LOCATION,
82
+ HTTP2_HEADER_RETRY_AFTER,
78
83
  HTTP2_HEADER_SET_COOKIE,
79
84
  HTTP2_HEADER_STATUS,
80
85
  HTTP2_METHOD_GET,
81
86
  HTTP2_METHOD_HEAD,
82
87
  HTTP_STATUS_BAD_REQUEST,
83
- HTTP_STATUS_SEE_OTHER
84
- } = _http.default.constants;
88
+ HTTP_STATUS_MOVED_PERMANENTLY,
89
+ HTTP_STATUS_SEE_OTHER,
90
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
91
+ HTTP_STATUS_TOO_MANY_REQUESTS
92
+ } = _http2.default.constants;
93
+ const maxRetryAfter = Symbol('maxRetryAfter');
94
+
95
+ const maxRetryAfterError = (interval, options) => new _errors.RequestError(`Maximum '${HTTP2_HEADER_RETRY_AFTER}' limit exceeded: ${interval} ms.`, options);
96
+
97
+ let defaults = {
98
+ follow: 20,
99
+
100
+ get maxRetryAfter() {
101
+ return this[maxRetryAfter] ?? this.timeout;
102
+ },
103
+
104
+ set maxRetryAfter(value) {
105
+ this[maxRetryAfter] = value;
106
+ },
107
+
108
+ method: HTTP2_METHOD_GET,
109
+ retry: {
110
+ attempts: 0,
111
+ backoffStrategy: 'interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)',
112
+ interval: 1e3,
113
+ retryAfter: true,
114
+ statusCodes: [HTTP_STATUS_TOO_MANY_REQUESTS, HTTP_STATUS_SERVICE_UNAVAILABLE]
115
+ },
116
+ timeout: 3e5
117
+ };
85
118
 
86
119
  async function rekwest(url, options = {}) {
87
120
  url = options.url = new URL(url);
88
121
 
89
122
  if (!options.redirected) {
90
- options = (0, _helpers.merge)(rekwest.defaults, {
91
- follow: 20,
92
- method: HTTP2_METHOD_GET
93
- }, options);
123
+ options = (0, _helpers.merge)(rekwest.defaults, options);
94
124
  }
95
125
 
96
126
  if (options.body && [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(options.method)) {
97
- throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body`);
127
+ throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body.`);
98
128
  }
99
129
 
100
- if (!options.follow) {
130
+ if (options.follow === 0) {
101
131
  throw new _errors.RequestError(`Maximum redirect reached at: ${url.href}`);
102
132
  }
103
133
 
@@ -115,8 +145,14 @@ async function rekwest(url, options = {}) {
115
145
  h2,
116
146
  redirect,
117
147
  redirected,
118
- thenable
148
+ thenable,
149
+ url: {
150
+ protocol
151
+ }
119
152
  } = options;
153
+ const {
154
+ request
155
+ } = protocol === 'http:' ? _http.default : _https.default;
120
156
  let {
121
157
  body
122
158
  } = options;
@@ -125,32 +161,28 @@ async function rekwest(url, options = {}) {
125
161
  body &&= (0, _helpers.transform)(body, options);
126
162
 
127
163
  if (h2) {
128
- client = _http.default.connect(url.origin, options);
164
+ client = _http2.default.connect(url.origin, options);
129
165
  req = client.request(options.headers, options);
130
166
  } else {
131
- req = (0, _https.request)(url, options);
167
+ req = request(url, options);
132
168
  }
133
169
 
134
- req.on('response', res => {
170
+ (0, _helpers.affix)(client, req, options);
171
+ req.once('error', reject);
172
+ req.once('frameError', reject);
173
+ req.once('goaway', reject);
174
+ req.once('response', res => {
175
+ let headers;
176
+
135
177
  if (h2) {
136
- const headers = res;
178
+ headers = res;
137
179
  res = req;
138
- Reflect.defineProperty(res, 'headers', {
139
- enumerable: true,
140
- value: headers
141
- });
142
- Reflect.defineProperty(res, 'httpVersion', {
143
- enumerable: true,
144
- value: `${h2 + 1}.0`
145
- });
146
- Reflect.defineProperty(res, 'statusCode', {
147
- enumerable: true,
148
- value: headers[HTTP2_HEADER_STATUS]
149
- });
150
180
  } else {
151
- res.on('error', reject);
181
+ res.once('error', reject);
152
182
  }
153
183
 
184
+ (0, _helpers.admix)(res, headers, options);
185
+
154
186
  if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
155
187
  if (_cookies.Cookies.jar.has(url.origin)) {
156
188
  new _cookies.Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]).forEach(function (val, key) {
@@ -167,15 +199,15 @@ async function rekwest(url, options = {}) {
167
199
  });
168
200
 
169
201
  if (follow && /^3\d{2}$/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
170
- if (redirect === 'error') {
171
- res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'`));
202
+ if (redirect === _helpers.redirects.error) {
203
+ res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'.`));
172
204
  }
173
205
 
174
- if (redirect === 'follow') {
206
+ if (redirect === _helpers.redirects.follow) {
175
207
  options.url = new URL(res.headers[HTTP2_HEADER_LOCATION], url).href;
176
208
 
177
209
  if (res.statusCode !== HTTP_STATUS_SEE_OTHER && body === Object(body) && body.pipe?.constructor === Function) {
178
- res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with body as readable stream`));
210
+ res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with body as readable stream.`));
179
211
  }
180
212
 
181
213
  options.follow--;
@@ -187,37 +219,29 @@ async function rekwest(url, options = {}) {
187
219
  }
188
220
 
189
221
  Reflect.set(options, 'redirected', true);
222
+
223
+ if (res.statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
224
+ let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
225
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
226
+
227
+ if (interval > options.maxRetryAfter) {
228
+ res.emit('error', maxRetryAfterError(interval, {
229
+ cause: (0, _helpers.mixin)(res, options)
230
+ }));
231
+ }
232
+
233
+ return (0, _promises.setTimeout)(interval).then(() => rekwest(options.url, options).then(resolve, reject));
234
+ }
235
+
190
236
  return rekwest(options.url, options).then(resolve, reject);
191
237
  }
192
238
  }
193
239
 
194
- Reflect.defineProperty(res, 'ok', {
195
- enumerable: true,
196
- value: /^2\d{2}$/.test(res.statusCode)
197
- });
198
- Reflect.defineProperty(res, 'redirected', {
199
- enumerable: true,
200
- value: options.redirected
201
- });
202
-
203
240
  if (res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
204
- return reject((0, _helpers.premix)(res, options));
241
+ return reject((0, _helpers.mixin)(res, options));
205
242
  }
206
243
 
207
- resolve((0, _helpers.premix)(res, options));
208
- });
209
- req.on('end', () => {
210
- client?.close();
211
- });
212
- req.on('error', reject);
213
- req.on('frameError', reject);
214
- req.on('goaway', reject);
215
- req.on('timeout', req.destroy);
216
- req.on('trailers', trailers => {
217
- Reflect.defineProperty(req, 'trailers', {
218
- enumerable: true,
219
- value: trailers
220
- });
244
+ resolve((0, _helpers.mixin)(res, options));
221
245
  });
222
246
  (0, _helpers.dispatch)(req, { ...options,
223
247
  body
@@ -233,6 +257,29 @@ async function rekwest(url, options = {}) {
233
257
 
234
258
  return res;
235
259
  } catch (ex) {
260
+ if (options.retry?.attempts && options.retry?.statusCodes.includes(ex.statusCode)) {
261
+ let {
262
+ interval
263
+ } = options.retry;
264
+
265
+ if (options.retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
266
+ interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
267
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
268
+
269
+ if (interval > options.maxRetryAfter) {
270
+ throw maxRetryAfterError(interval, {
271
+ cause: ex
272
+ });
273
+ }
274
+ } else {
275
+ interval = new Function('interval', `return Math.ceil(${options.retry.backoffStrategy});`)(interval);
276
+ }
277
+
278
+ options.retry.attempts--;
279
+ options.retry.interval = interval;
280
+ return (0, _promises.setTimeout)(interval).then(() => rekwest(url, options));
281
+ }
282
+
236
283
  if (digest && !redirected && ex.body) {
237
284
  ex.body = await ex.body();
238
285
  }
@@ -254,24 +301,50 @@ Reflect.defineProperty(rekwest, 'stream', {
254
301
  headers: {
255
302
  [HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_OCTET_STREAM
256
303
  }
257
- }, options)
304
+ }, options),
305
+ redirect: _helpers.redirects.manual
258
306
  });
307
+ const {
308
+ h2,
309
+ url: {
310
+ protocol
311
+ }
312
+ } = options;
313
+ const {
314
+ request
315
+ } = protocol === 'http:' ? _http.default : _https.default;
316
+ let client, req;
259
317
 
260
- if (options.h2) {
261
- const client = _http.default.connect(url.origin, options);
262
-
263
- const req = client.request(options.headers, options);
264
- req.on('end', () => {
265
- client.close();
266
- });
267
- return req;
318
+ if (h2) {
319
+ client = _http2.default.connect(url.origin, options);
320
+ req = client.request(options.headers, options);
321
+ } else {
322
+ req = request(options.url, options);
268
323
  }
269
324
 
270
- return (0, _https.request)(options.url, options);
325
+ (0, _helpers.affix)(client, req, options);
326
+ req.once('response', res => {
327
+ let headers;
328
+
329
+ if (h2) {
330
+ headers = res;
331
+ res = req;
332
+ }
333
+
334
+ (0, _helpers.admix)(res, headers, options);
335
+ });
336
+ return req;
271
337
  }
272
338
  });
273
339
  Reflect.defineProperty(rekwest, 'defaults', {
274
340
  enumerable: true,
275
- value: Object.create(null),
276
- writable: true
341
+
342
+ get() {
343
+ return defaults;
344
+ },
345
+
346
+ set(value) {
347
+ defaults = (0, _helpers.merge)(defaults, value);
348
+ }
349
+
277
350
  });