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/src/index.mjs CHANGED
@@ -1,14 +1,17 @@
1
1
  import http from 'http';
2
2
  import http2 from 'http2';
3
3
  import https from 'https';
4
+ import { setTimeout as setTimeoutPromise } from 'timers/promises';
4
5
  import { ackn } from './ackn.mjs';
5
6
  import { Cookies } from './cookies.mjs';
6
7
  import { RequestError } from './errors.mjs';
7
8
  import {
9
+ admix,
10
+ affix,
8
11
  dispatch,
9
12
  merge,
13
+ mixin,
10
14
  preflight,
11
- premix,
12
15
  redirects,
13
16
  transform,
14
17
  } from './helpers.mjs';
@@ -27,18 +30,48 @@ const {
27
30
  HTTP2_HEADER_CONTENT_LENGTH,
28
31
  HTTP2_HEADER_CONTENT_TYPE,
29
32
  HTTP2_HEADER_LOCATION,
33
+ HTTP2_HEADER_RETRY_AFTER,
30
34
  HTTP2_HEADER_SET_COOKIE,
31
- HTTP2_HEADER_STATUS,
32
35
  HTTP2_METHOD_GET,
33
36
  HTTP2_METHOD_HEAD,
34
37
  HTTP_STATUS_BAD_REQUEST,
38
+ HTTP_STATUS_MOVED_PERMANENTLY,
35
39
  HTTP_STATUS_SEE_OTHER,
40
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
41
+ HTTP_STATUS_TOO_MANY_REQUESTS,
36
42
  } = http2.constants;
37
43
 
44
+ const maxRetryAfter = Symbol('maxRetryAfter');
45
+ const maxRetryAfterError = (
46
+ interval,
47
+ options,
48
+ ) => new RequestError(`Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms.`, options);
49
+ let defaults = {
50
+ follow: 20,
51
+ get maxRetryAfter() {
52
+ return this[maxRetryAfter] ?? this.timeout;
53
+ },
54
+ set maxRetryAfter(value) {
55
+ this[maxRetryAfter] = value;
56
+ },
57
+ method: HTTP2_METHOD_GET,
58
+ retry: {
59
+ attempts: 0,
60
+ backoffStrategy: 'interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)',
61
+ interval: 1e3,
62
+ retryAfter: true,
63
+ statusCodes: [
64
+ HTTP_STATUS_TOO_MANY_REQUESTS,
65
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
66
+ ],
67
+ },
68
+ timeout: 3e5,
69
+ };
70
+
38
71
  export default async function rekwest(url, options = {}) {
39
72
  url = options.url = new URL(url);
40
73
  if (!options.redirected) {
41
- options = merge(rekwest.defaults, { follow: 20, method: HTTP2_METHOD_GET }, options);
74
+ options = merge(rekwest.defaults, options);
42
75
  }
43
76
 
44
77
  if (options.body && [
@@ -48,7 +81,7 @@ export default async function rekwest(url, options = {}) {
48
81
  throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body.`);
49
82
  }
50
83
 
51
- if (!options.follow) {
84
+ if (options.follow === 0) {
52
85
  throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
53
86
  }
54
87
 
@@ -81,30 +114,22 @@ export default async function rekwest(url, options = {}) {
81
114
  req = request(url, options);
82
115
  }
83
116
 
84
- req.on('response', (res) => {
85
- if (h2) {
86
- const headers = res;
117
+ affix(client, req, options);
118
+ req.once('error', reject);
119
+ req.once('frameError', reject);
120
+ req.once('goaway', reject);
121
+ req.once('response', (res) => {
122
+ let headers;
87
123
 
124
+ if (h2) {
125
+ headers = res;
88
126
  res = req;
89
-
90
- Reflect.defineProperty(res, 'headers', {
91
- enumerable: true,
92
- value: headers,
93
- });
94
-
95
- Reflect.defineProperty(res, 'httpVersion', {
96
- enumerable: true,
97
- value: `${ h2 + 1 }.0`,
98
- });
99
-
100
- Reflect.defineProperty(res, 'statusCode', {
101
- enumerable: true,
102
- value: headers[HTTP2_HEADER_STATUS],
103
- });
104
127
  } else {
105
- res.on('error', reject);
128
+ res.once('error', reject);
106
129
  }
107
130
 
131
+ admix(res, headers, options);
132
+
108
133
  if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
109
134
  if (Cookies.jar.has(url.origin)) {
110
135
  new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]).forEach(function (val, key) {
@@ -145,39 +170,27 @@ export default async function rekwest(url, options = {}) {
145
170
 
146
171
  Reflect.set(options, 'redirected', true);
147
172
 
173
+ if (res.statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
174
+ let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
175
+
176
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
177
+
178
+ if (interval > options.maxRetryAfter) {
179
+ res.emit('error', maxRetryAfterError(interval, { cause: mixin(res, options) }));
180
+ }
181
+
182
+ return setTimeoutPromise(interval).then(() => rekwest(options.url, options).then(resolve, reject));
183
+ }
184
+
148
185
  return rekwest(options.url, options).then(resolve, reject);
149
186
  }
150
187
  }
151
188
 
152
- Reflect.defineProperty(res, 'ok', {
153
- enumerable: true,
154
- value: /^2\d{2}$/.test(res.statusCode),
155
- });
156
-
157
- Reflect.defineProperty(res, 'redirected', {
158
- enumerable: true,
159
- value: options.redirected,
160
- });
161
-
162
189
  if (res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
163
- return reject(premix(res, options));
190
+ return reject(mixin(res, options));
164
191
  }
165
192
 
166
- resolve(premix(res, options));
167
- });
168
-
169
- req.on('end', () => {
170
- client?.close();
171
- });
172
- req.on('error', reject);
173
- req.on('frameError', reject);
174
- req.on('goaway', reject);
175
- req.on('timeout', req.destroy);
176
- req.on('trailers', (trailers) => {
177
- Reflect.defineProperty(req, 'trailers', {
178
- enumerable: true,
179
- value: trailers,
180
- });
193
+ resolve(mixin(res, options));
181
194
  });
182
195
 
183
196
  dispatch(req, { ...options, body });
@@ -192,6 +205,27 @@ export default async function rekwest(url, options = {}) {
192
205
 
193
206
  return res;
194
207
  } catch (ex) {
208
+ const { maxRetryAfter, retry } = options;
209
+
210
+ if (retry?.attempts && retry?.statusCodes.includes(ex.statusCode)) {
211
+ let { interval } = retry;
212
+
213
+ if (retry.retryAfter && ex.headers[HTTP2_HEADER_RETRY_AFTER]) {
214
+ interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
215
+ interval = Number(interval) * 1000 || new Date(interval) - Date.now();
216
+ if (interval > maxRetryAfter) {
217
+ throw maxRetryAfterError(interval, { cause: ex });
218
+ }
219
+ } else {
220
+ interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
221
+ }
222
+
223
+ retry.attempts--;
224
+ retry.interval = interval;
225
+
226
+ return setTimeoutPromise(interval).then(() => rekwest(url, options));
227
+ }
228
+
195
229
  if (digest && !redirected && ex.body) {
196
230
  ex.body = await ex.body();
197
231
  }
@@ -215,26 +249,35 @@ Reflect.defineProperty(rekwest, 'stream', {
215
249
  redirect: redirects.manual,
216
250
  });
217
251
 
218
- if (options.h2) {
219
- const client = http2.connect(url.origin, options);
220
- const req = client.request(options.headers, options);
221
-
222
- req.on('end', () => {
223
- client.close();
224
- });
252
+ const { h2, url: { protocol } } = options;
253
+ const { request } = (protocol === 'http:' ? http : https);
254
+ let client, req;
225
255
 
226
- return req;
256
+ if (h2) {
257
+ client = http2.connect(url.origin, options);
258
+ req = client.request(options.headers, options);
259
+ } else {
260
+ req = request(options.url, options);
227
261
  }
228
262
 
229
- const { url: { protocol } } = options;
230
- const { request } = (protocol === 'http:' ? http : https);
263
+ affix(client, req, options);
264
+ req.once('response', (res) => {
265
+ let headers;
266
+
267
+ if (h2) {
268
+ headers = res;
269
+ res = req;
270
+ }
271
+
272
+ admix(res, headers, options);
273
+ });
231
274
 
232
- return request(options.url, options);
275
+ return req;
233
276
  },
234
277
  });
235
278
 
236
279
  Reflect.defineProperty(rekwest, 'defaults', {
237
280
  enumerable: true,
238
- value: Object.create(null),
239
- writable: true,
281
+ get() { return defaults; },
282
+ set(value) { defaults = merge(defaults, value); },
240
283
  });