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