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/src/index.mjs CHANGED
@@ -1,13 +1,18 @@
1
+ import http from 'http';
1
2
  import http2 from 'http2';
2
- import { request } from 'https';
3
+ import https from 'https';
4
+ import { setTimeout as setTimeoutPromise } from 'timers/promises';
3
5
  import { ackn } from './ackn.mjs';
4
6
  import { Cookies } from './cookies.mjs';
5
7
  import { RequestError } from './errors.mjs';
6
8
  import {
9
+ admix,
10
+ affix,
7
11
  dispatch,
8
12
  merge,
13
+ mixin,
9
14
  preflight,
10
- premix,
15
+ redirects,
11
16
  transform,
12
17
  } from './helpers.mjs';
13
18
  import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
@@ -25,28 +30,59 @@ const {
25
30
  HTTP2_HEADER_CONTENT_LENGTH,
26
31
  HTTP2_HEADER_CONTENT_TYPE,
27
32
  HTTP2_HEADER_LOCATION,
33
+ HTTP2_HEADER_RETRY_AFTER,
28
34
  HTTP2_HEADER_SET_COOKIE,
29
35
  HTTP2_HEADER_STATUS,
30
36
  HTTP2_METHOD_GET,
31
37
  HTTP2_METHOD_HEAD,
32
38
  HTTP_STATUS_BAD_REQUEST,
39
+ HTTP_STATUS_MOVED_PERMANENTLY,
33
40
  HTTP_STATUS_SEE_OTHER,
41
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
42
+ HTTP_STATUS_TOO_MANY_REQUESTS,
34
43
  } = http2.constants;
35
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
+
36
72
  export default async function rekwest(url, options = {}) {
37
73
  url = options.url = new URL(url);
38
74
  if (!options.redirected) {
39
- options = merge(rekwest.defaults, { follow: 20, method: HTTP2_METHOD_GET }, options);
75
+ options = merge(rekwest.defaults, options);
40
76
  }
41
77
 
42
78
  if (options.body && [
43
79
  HTTP2_METHOD_GET,
44
80
  HTTP2_METHOD_HEAD,
45
81
  ].includes(options.method)) {
46
- throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body`);
82
+ throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body.`);
47
83
  }
48
84
 
49
- if (!options.follow) {
85
+ if (options.follow === 0) {
50
86
  throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
51
87
  }
52
88
 
@@ -63,7 +99,8 @@ export default async function rekwest(url, options = {}) {
63
99
 
64
100
  options = preflight(options);
65
101
 
66
- const { cookies, digest, follow, h2, redirect, redirected, thenable } = options;
102
+ const { cookies, digest, follow, h2, redirect, redirected, thenable, url: { protocol } } = options;
103
+ const { request } = (protocol === 'http:' ? http : https);
67
104
  let { body } = options;
68
105
 
69
106
  const promise = new Promise((resolve, reject) => {
@@ -78,30 +115,22 @@ export default async function rekwest(url, options = {}) {
78
115
  req = request(url, options);
79
116
  }
80
117
 
81
- req.on('response', (res) => {
82
- if (h2) {
83
- 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;
84
124
 
125
+ if (h2) {
126
+ headers = res;
85
127
  res = req;
86
-
87
- Reflect.defineProperty(res, 'headers', {
88
- enumerable: true,
89
- value: headers,
90
- });
91
-
92
- Reflect.defineProperty(res, 'httpVersion', {
93
- enumerable: true,
94
- value: `${ h2 + 1 }.0`,
95
- });
96
-
97
- Reflect.defineProperty(res, 'statusCode', {
98
- enumerable: true,
99
- value: headers[HTTP2_HEADER_STATUS],
100
- });
101
128
  } else {
102
- res.on('error', reject);
129
+ res.once('error', reject);
103
130
  }
104
131
 
132
+ admix(res, headers, options);
133
+
105
134
  if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
106
135
  if (Cookies.jar.has(url.origin)) {
107
136
  new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE]).forEach(function (val, key) {
@@ -120,16 +149,16 @@ export default async function rekwest(url, options = {}) {
120
149
  });
121
150
 
122
151
  if (follow && /^3\d{2}$/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
123
- if (redirect === 'error') {
124
- res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'`));
152
+ if (redirect === redirects.error) {
153
+ res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
125
154
  }
126
155
 
127
- if (redirect === 'follow') {
156
+ if (redirect === redirects.follow) {
128
157
  options.url = new URL(res.headers[HTTP2_HEADER_LOCATION], url).href;
129
158
 
130
159
  if (res.statusCode !== HTTP_STATUS_SEE_OTHER
131
160
  && body === Object(body) && body.pipe?.constructor === Function) {
132
- res.emit('error', new RequestError(`Unable to ${ redirect } redirect with body as readable stream`));
161
+ res.emit('error', new RequestError(`Unable to ${ redirect } redirect with body as readable stream.`));
133
162
  }
134
163
 
135
164
  options.follow--;
@@ -142,39 +171,27 @@ export default async function rekwest(url, options = {}) {
142
171
 
143
172
  Reflect.set(options, 'redirected', true);
144
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
+
145
186
  return rekwest(options.url, options).then(resolve, reject);
146
187
  }
147
188
  }
148
189
 
149
- Reflect.defineProperty(res, 'ok', {
150
- enumerable: true,
151
- value: /^2\d{2}$/.test(res.statusCode),
152
- });
153
-
154
- Reflect.defineProperty(res, 'redirected', {
155
- enumerable: true,
156
- value: options.redirected,
157
- });
158
-
159
190
  if (res.statusCode >= HTTP_STATUS_BAD_REQUEST) {
160
- return reject(premix(res, options));
191
+ return reject(mixin(res, options));
161
192
  }
162
193
 
163
- resolve(premix(res, options));
164
- });
165
-
166
- req.on('end', () => {
167
- client?.close();
168
- });
169
- req.on('error', reject);
170
- req.on('frameError', reject);
171
- req.on('goaway', reject);
172
- req.on('timeout', req.destroy);
173
- req.on('trailers', (trailers) => {
174
- Reflect.defineProperty(req, 'trailers', {
175
- enumerable: true,
176
- value: trailers,
177
- });
194
+ resolve(mixin(res, options));
178
195
  });
179
196
 
180
197
  dispatch(req, { ...options, body });
@@ -189,6 +206,25 @@ export default async function rekwest(url, options = {}) {
189
206
 
190
207
  return res;
191
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
+
192
228
  if (digest && !redirected && ex.body) {
193
229
  ex.body = await ex.body();
194
230
  }
@@ -209,25 +245,38 @@ Reflect.defineProperty(rekwest, 'stream', {
209
245
  ...merge(rekwest.defaults, {
210
246
  headers: { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_OCTET_STREAM },
211
247
  }, options),
248
+ redirect: redirects.manual,
212
249
  });
213
250
 
214
- if (options.h2) {
215
- const client = http2.connect(url.origin, options);
216
- const req = client.request(options.headers, options);
217
-
218
- req.on('end', () => {
219
- client.close();
220
- });
251
+ const { h2, url: { protocol } } = options;
252
+ const { request } = (protocol === 'http:' ? http : https);
253
+ let client, req;
221
254
 
222
- 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);
223
260
  }
224
261
 
225
- return request(options.url, options);
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
+ });
273
+
274
+ return req;
226
275
  },
227
276
  });
228
277
 
229
278
  Reflect.defineProperty(rekwest, 'defaults', {
230
279
  enumerable: true,
231
- value: Object.create(null),
232
- writable: true,
280
+ get() { return defaults; },
281
+ set(value) { defaults = merge(defaults, value); },
233
282
  });