rekwest 5.2.5 → 5.2.7

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/package.json CHANGED
@@ -8,15 +8,15 @@
8
8
  "url": "https://github.com/bricss/rekwest/issues"
9
9
  },
10
10
  "devDependencies": {
11
- "@babel/cli": "^7.24.7",
12
- "@babel/core": "^7.24.7",
13
- "@babel/eslint-parser": "^7.24.7",
14
- "@babel/preset-env": "^7.24.7",
15
- "@stylistic/eslint-plugin-js": "^2.1.0",
11
+ "@babel/cli": "^7.25.6",
12
+ "@babel/core": "^7.25.2",
13
+ "@babel/eslint-parser": "^7.25.1",
14
+ "@babel/preset-env": "^7.25.4",
15
+ "@stylistic/eslint-plugin-js": "^2.7.2",
16
16
  "c8": "^10.1.2",
17
- "eslint": "^9.4.0",
18
- "eslint-config-ultra-refined": "^3.1.2",
19
- "mocha": "^10.4.0"
17
+ "eslint": "^9.10.0",
18
+ "eslint-config-ultra-refined": "^3.2.5",
19
+ "mocha": "^10.7.3"
20
20
  },
21
21
  "description": "The robust request library that humanity deserves 🌐",
22
22
  "engines": {
@@ -70,5 +70,5 @@
70
70
  "test:bail": "mocha --bail",
71
71
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
72
72
  },
73
- "version": "5.2.5"
73
+ "version": "5.2.7"
74
74
  }
package/src/errors.mjs CHANGED
@@ -1,18 +1,18 @@
1
- export class RequestError extends Error {
2
-
3
- get [Symbol.toStringTag]() {
4
- return this.constructor.name;
5
- }
6
-
7
- get name() {
8
- return this[Symbol.toStringTag];
9
- }
10
-
11
- constructor(...args) {
12
- super(...args);
13
- Error.captureStackTrace(this, this.constructor);
14
- }
15
-
16
- }
17
-
18
- export class TimeoutError extends RequestError {}
1
+ export class RequestError extends Error {
2
+
3
+ get [Symbol.toStringTag]() {
4
+ return this.constructor.name;
5
+ }
6
+
7
+ get name() {
8
+ return this[Symbol.toStringTag];
9
+ }
10
+
11
+ constructor(...args) {
12
+ super(...args);
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+
16
+ }
17
+
18
+ export class TimeoutError extends RequestError {}
package/src/index.mjs CHANGED
@@ -1,87 +1,87 @@
1
- import http from 'node:http';
2
- import http2 from 'node:http2';
3
- import https from 'node:https';
4
- import { requestRedirect } from './constants.mjs';
5
- import defaults from './defaults.mjs';
6
- import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
7
- import { preflight } from './preflight.mjs';
8
- import { transfer } from './transfer.mjs';
9
- import {
10
- admix,
11
- affix,
12
- copyWithMerge,
13
- normalize,
14
- } from './utils.mjs';
15
- import { validation } from './validation.mjs';
16
-
17
- export {
18
- Blob,
19
- File,
20
- } from 'node:buffer';
21
- export { constants } from 'node:http2';
22
-
23
- export * from './ackn.mjs';
24
- export * from './constants.mjs';
25
- export * from './cookies.mjs';
26
- export * from './errors.mjs';
27
- export * from './formdata.mjs';
28
- export * as mediatypes from './mediatypes.mjs';
29
- export * from './mixin.mjs';
30
- export * from './utils.mjs';
31
- export * from './validation.mjs';
32
-
33
- const {
34
- HTTP2_HEADER_CONTENT_TYPE,
35
- } = http2.constants;
36
-
37
- export default function rekwest(url, options) {
38
- return transfer(validation(normalize(url, options)), rekwest);
39
- }
40
-
41
- Reflect.defineProperty(rekwest, 'defaults', {
42
- enumerable: true,
43
- get() { return defaults.stash; },
44
- set(value) { defaults.stash = copyWithMerge(defaults.stash, value); },
45
- });
46
-
47
- Reflect.defineProperty(rekwest, 'extend', {
48
- enumerable: true,
49
- value(options) {
50
- return (url, opts) => rekwest(url, copyWithMerge(options, opts));
51
- },
52
- });
53
-
54
- Reflect.defineProperty(rekwest, 'stream', {
55
- enumerable: true,
56
- value(url, options) {
57
- options = preflight(validation(normalize(url, copyWithMerge({}, options, {
58
- headers: { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_OCTET_STREAM },
59
- redirect: requestRedirect.manual,
60
- }))));
61
- let client, req;
62
-
63
- if (options.h2) {
64
- client = http2.connect(options.url.origin, options);
65
- req = client.request(options.headers, options);
66
- } else {
67
- const { request } = options.url.protocol === 'http:' ? http : https;
68
-
69
- req = request(options.url, options);
70
- }
71
-
72
- affix(client, req, options);
73
-
74
- req.once('response', (res) => {
75
- let headers;
76
-
77
- if (options.h2) {
78
- headers = res;
79
- res = req;
80
- }
81
-
82
- admix(res, headers, options);
83
- });
84
-
85
- return req;
86
- },
87
- });
1
+ import http from 'node:http';
2
+ import http2 from 'node:http2';
3
+ import https from 'node:https';
4
+ import { requestRedirect } from './constants.mjs';
5
+ import defaults from './defaults.mjs';
6
+ import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
7
+ import { preflight } from './preflight.mjs';
8
+ import { transfer } from './transfer.mjs';
9
+ import {
10
+ admix,
11
+ affix,
12
+ copyWithMerge,
13
+ normalize,
14
+ } from './utils.mjs';
15
+ import { validation } from './validation.mjs';
16
+
17
+ export {
18
+ Blob,
19
+ File,
20
+ } from 'node:buffer';
21
+ export { constants } from 'node:http2';
22
+
23
+ export * from './ackn.mjs';
24
+ export * from './constants.mjs';
25
+ export * from './cookies.mjs';
26
+ export * from './errors.mjs';
27
+ export * from './formdata.mjs';
28
+ export * as mediatypes from './mediatypes.mjs';
29
+ export * from './mixin.mjs';
30
+ export * from './utils.mjs';
31
+ export * from './validation.mjs';
32
+
33
+ const {
34
+ HTTP2_HEADER_CONTENT_TYPE,
35
+ } = http2.constants;
36
+
37
+ export default function rekwest(url, options) {
38
+ return transfer(validation(normalize(url, options)), rekwest);
39
+ }
40
+
41
+ Reflect.defineProperty(rekwest, 'defaults', {
42
+ enumerable: true,
43
+ get() { return defaults.stash; },
44
+ set(value) { defaults.stash = copyWithMerge(defaults.stash, value); },
45
+ });
46
+
47
+ Reflect.defineProperty(rekwest, 'extend', {
48
+ enumerable: true,
49
+ value(options) {
50
+ return (url, opts) => rekwest(url, copyWithMerge(options, opts));
51
+ },
52
+ });
53
+
54
+ Reflect.defineProperty(rekwest, 'stream', {
55
+ enumerable: true,
56
+ value(url, options) {
57
+ options = preflight(validation(normalize(url, copyWithMerge({}, options, {
58
+ headers: { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_OCTET_STREAM },
59
+ redirect: requestRedirect.manual,
60
+ }))));
61
+ let client, req;
62
+
63
+ if (options.h2) {
64
+ client = http2.connect(options.url.origin, options);
65
+ req = client.request(options.headers, options);
66
+ } else {
67
+ const { request } = options.url.protocol === 'http:' ? http : https;
68
+
69
+ req = request(options.url, options);
70
+ }
71
+
72
+ affix(client, req, options);
73
+
74
+ req.once('response', (res) => {
75
+ let headers;
76
+
77
+ if (options.h2) {
78
+ headers = res;
79
+ res = req;
80
+ }
81
+
82
+ admix(res, headers, options);
83
+ });
84
+
85
+ return req;
86
+ },
87
+ });
@@ -1,136 +1,136 @@
1
- import http2 from 'node:http2';
2
- import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
3
- import {
4
- requestCredentials,
5
- requestRedirect,
6
- requestRedirectCodes,
7
- } from './constants.mjs';
8
- import { Cookies } from './cookies.mjs';
9
- import { RequestError } from './errors.mjs';
10
- import rekwest from './index.mjs';
11
- import { mixin } from './mixin.mjs';
12
- import {
13
- admix,
14
- maxRetryAfterError,
15
- sameOrigin,
16
- } from './utils.mjs';
17
-
18
- const {
19
- HTTP2_HEADER_LOCATION,
20
- HTTP2_HEADER_RETRY_AFTER,
21
- HTTP2_HEADER_SET_COOKIE,
22
- HTTP2_METHOD_GET,
23
- HTTP2_METHOD_HEAD,
24
- HTTP2_METHOD_POST,
25
- HTTP_STATUS_BAD_REQUEST,
26
- HTTP_STATUS_FOUND,
27
- HTTP_STATUS_MOVED_PERMANENTLY,
28
- HTTP_STATUS_SEE_OTHER,
29
- } = http2.constants;
30
-
31
- export const postflight = (req, res, options, { reject, resolve }) => {
32
- const { cookies, credentials, follow, h2, redirect, url } = options;
33
- let headers;
34
-
35
- if (h2) {
36
- headers = res;
37
- res = req;
38
- } else {
39
- res.once('error', reject);
40
- }
41
-
42
- admix(res, headers, options);
43
-
44
- if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
45
- if (Cookies.jar.has(url.origin)) {
46
- const cookie = new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options);
47
-
48
- Cookies.jar.get(url.origin).forEach((val, key) => {
49
- if (!cookie.has(key)) {
50
- cookie.set(key, val);
51
- }
52
- });
53
- Cookies.jar.set(url.origin, cookie);
54
- } else {
55
- Cookies.jar.set(url.origin, new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options));
56
- }
57
- }
58
-
59
- Reflect.defineProperty(res, 'cookies', {
60
- enumerable: true,
61
- value: cookies !== false && Cookies.jar.has(url.origin)
62
- ? Cookies.jar.get(url.origin)
63
- : void 0,
64
- });
65
-
66
- const { statusCode } = res;
67
-
68
- if (follow && /3\d{2}/.test(statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
69
- if (!requestRedirectCodes.includes(statusCode)) {
70
- return res.emit('error', new RangeError(`Invalid status code: ${ statusCode }`));
71
- }
72
-
73
- if (redirect === requestRedirect.error) {
74
- return res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
75
- }
76
-
77
- if (redirect === requestRedirect.follow) {
78
- const location = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
79
-
80
- if (!/^https?:/i.test(location.protocol)) {
81
- return res.emit('error', new RequestError('URL scheme must be "http" or "https".'));
82
- }
83
-
84
- if (!sameOrigin(location, url)) {
85
- if (credentials !== requestCredentials.include) {
86
- options.credentials = requestCredentials.omit;
87
- }
88
-
89
- options.h2 = false;
90
- }
91
-
92
- if (statusCode !== HTTP_STATUS_SEE_OTHER && options.body?.pipe?.constructor === Function) {
93
- return res.emit('error', new RequestError(`Unable to ${ redirect } redirect with streamable body.`));
94
- }
95
-
96
- if (([
97
- HTTP_STATUS_MOVED_PERMANENTLY,
98
- HTTP_STATUS_FOUND,
99
- ].includes(statusCode) && options.method === HTTP2_METHOD_POST) || (statusCode === HTTP_STATUS_SEE_OTHER && ![
100
- HTTP2_METHOD_GET,
101
- HTTP2_METHOD_HEAD,
102
- ].includes(options.method))) {
103
- for (const it of Object.keys(options.headers).filter((val) => /^content-/i.test(val))) {
104
- Reflect.deleteProperty(options.headers, it);
105
- }
106
-
107
- options.body = null;
108
- options.method = HTTP2_METHOD_GET;
109
- }
110
-
111
- options.follow--;
112
- options.redirected = true;
113
- options.url = location;
114
-
115
- if (statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
116
- let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
117
-
118
- interval = Number(interval) * 1e3 || new Date(interval) - Date.now();
119
-
120
- if (interval > options.maxRetryAfter) {
121
- return res.emit('error', maxRetryAfterError(interval, { cause: mixin(res, options) }));
122
- }
123
-
124
- return setTimeoutPromise(interval).then(() => rekwest(options.url, options).then(resolve, reject));
125
- }
126
-
127
- return rekwest(options.url, options).then(resolve, reject);
128
- }
129
- }
130
-
131
- if (statusCode >= HTTP_STATUS_BAD_REQUEST) {
132
- return reject(mixin(res, options));
133
- }
134
-
135
- resolve(mixin(res, options));
136
- };
1
+ import http2 from 'node:http2';
2
+ import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
3
+ import {
4
+ requestCredentials,
5
+ requestRedirect,
6
+ requestRedirectCodes,
7
+ } from './constants.mjs';
8
+ import { Cookies } from './cookies.mjs';
9
+ import { RequestError } from './errors.mjs';
10
+ import rekwest from './index.mjs';
11
+ import { mixin } from './mixin.mjs';
12
+ import {
13
+ admix,
14
+ maxRetryAfterError,
15
+ sameOrigin,
16
+ } from './utils.mjs';
17
+
18
+ const {
19
+ HTTP2_HEADER_LOCATION,
20
+ HTTP2_HEADER_RETRY_AFTER,
21
+ HTTP2_HEADER_SET_COOKIE,
22
+ HTTP2_METHOD_GET,
23
+ HTTP2_METHOD_HEAD,
24
+ HTTP2_METHOD_POST,
25
+ HTTP_STATUS_BAD_REQUEST,
26
+ HTTP_STATUS_FOUND,
27
+ HTTP_STATUS_MOVED_PERMANENTLY,
28
+ HTTP_STATUS_SEE_OTHER,
29
+ } = http2.constants;
30
+
31
+ export const postflight = (req, res, options, { reject, resolve }) => {
32
+ const { cookies, credentials, follow, h2, redirect, url } = options;
33
+ let headers;
34
+
35
+ if (h2) {
36
+ headers = res;
37
+ res = req;
38
+ } else {
39
+ res.once('error', reject);
40
+ }
41
+
42
+ admix(res, headers, options);
43
+
44
+ if (cookies !== false && res.headers[HTTP2_HEADER_SET_COOKIE]) {
45
+ if (Cookies.jar.has(url.origin)) {
46
+ const cookie = new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options);
47
+
48
+ Cookies.jar.get(url.origin).forEach((val, key) => {
49
+ if (!cookie.has(key)) {
50
+ cookie.set(key, val);
51
+ }
52
+ });
53
+ Cookies.jar.set(url.origin, cookie);
54
+ } else {
55
+ Cookies.jar.set(url.origin, new Cookies(res.headers[HTTP2_HEADER_SET_COOKIE], options));
56
+ }
57
+ }
58
+
59
+ Reflect.defineProperty(res, 'cookies', {
60
+ enumerable: true,
61
+ value: cookies !== false && Cookies.jar.has(url.origin)
62
+ ? Cookies.jar.get(url.origin)
63
+ : void 0,
64
+ });
65
+
66
+ const { statusCode } = res;
67
+
68
+ if (follow && /3\d{2}/.test(statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
69
+ if (!requestRedirectCodes.includes(statusCode)) {
70
+ return res.emit('error', new RangeError(`Invalid status code: ${ statusCode }`));
71
+ }
72
+
73
+ if (redirect === requestRedirect.error) {
74
+ return res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
75
+ }
76
+
77
+ if (redirect === requestRedirect.follow) {
78
+ const location = new URL(res.headers[HTTP2_HEADER_LOCATION], url);
79
+
80
+ if (!/^https?:/i.test(location.protocol)) {
81
+ return res.emit('error', new RequestError('URL scheme must be "http" or "https".'));
82
+ }
83
+
84
+ if (!sameOrigin(location, url)) {
85
+ if (credentials !== requestCredentials.include) {
86
+ options.credentials = requestCredentials.omit;
87
+ }
88
+
89
+ options.h2 = false;
90
+ }
91
+
92
+ if (statusCode !== HTTP_STATUS_SEE_OTHER && options.body?.pipe?.constructor === Function) {
93
+ return res.emit('error', new RequestError(`Unable to ${ redirect } redirect with streamable body.`));
94
+ }
95
+
96
+ if (([
97
+ HTTP_STATUS_MOVED_PERMANENTLY,
98
+ HTTP_STATUS_FOUND,
99
+ ].includes(statusCode) && options.method === HTTP2_METHOD_POST) || (statusCode === HTTP_STATUS_SEE_OTHER && ![
100
+ HTTP2_METHOD_GET,
101
+ HTTP2_METHOD_HEAD,
102
+ ].includes(options.method))) {
103
+ for (const it of Object.keys(options.headers).filter((val) => /^content-/i.test(val))) {
104
+ Reflect.deleteProperty(options.headers, it);
105
+ }
106
+
107
+ options.body = null;
108
+ options.method = HTTP2_METHOD_GET;
109
+ }
110
+
111
+ options.follow--;
112
+ options.redirected = true;
113
+ options.url = location;
114
+
115
+ if (statusCode === HTTP_STATUS_MOVED_PERMANENTLY && res.headers[HTTP2_HEADER_RETRY_AFTER]) {
116
+ let interval = res.headers[HTTP2_HEADER_RETRY_AFTER];
117
+
118
+ interval = Number(interval) * 1e3 || new Date(interval) - Date.now();
119
+
120
+ if (interval > options.maxRetryAfter) {
121
+ return res.emit('error', maxRetryAfterError(interval, { cause: mixin(res, options) }));
122
+ }
123
+
124
+ return setTimeoutPromise(interval).then(() => rekwest(options.url, options).then(resolve, reject));
125
+ }
126
+
127
+ return rekwest(options.url, options).then(resolve, reject);
128
+ }
129
+ }
130
+
131
+ if (statusCode >= HTTP_STATUS_BAD_REQUEST) {
132
+ return reject(mixin(res, options));
133
+ }
134
+
135
+ resolve(mixin(res, options));
136
+ };
package/src/transfer.mjs CHANGED
@@ -1,117 +1,117 @@
1
- import http from 'node:http';
2
- import http2 from 'node:http2';
3
- import https from 'node:https';
4
- import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
5
- import { ackn } from './ackn.mjs';
6
- import { RequestError } from './errors.mjs';
7
- import { postflight } from './postflight.mjs';
8
- import { preflight } from './preflight.mjs';
9
- import { transform } from './transform.mjs';
10
- import {
11
- affix,
12
- dispatch,
13
- maxRetryAfterError,
14
- } from './utils.mjs';
15
-
16
- const {
17
- HTTP2_HEADER_RETRY_AFTER,
18
- } = http2.constants;
19
-
20
- export const transfer = async (options, overact) => {
21
- const { digest, redirected, thenable, url } = options;
22
-
23
- if (options.follow === 0) {
24
- throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
25
- }
26
-
27
- if (url.protocol === 'https:') {
28
- options = !options.h2 ? await ackn(options) : {
29
- ...options,
30
- createConnection: null,
31
- protocol: url.protocol,
32
- };
33
- } else if (Reflect.has(options, 'alpnProtocol')) {
34
- for (const it of [
35
- 'alpnProtocol',
36
- 'createConnection',
37
- 'h2',
38
- 'protocol',
39
- ]) { Reflect.deleteProperty(options, it); }
40
- }
41
-
42
- try {
43
- options = await transform(preflight(options));
44
- } catch (ex) {
45
- options.createConnection?.().destroy();
46
- throw ex;
47
- }
48
-
49
- const promise = new Promise((resolve, reject) => {
50
- let client, req;
51
-
52
- if (options.h2) {
53
- client = http2.connect(url.origin, options);
54
- req = client.request(options.headers, options);
55
- } else {
56
- const { request } = url.protocol === 'http:' ? http : https;
57
-
58
- req = request(url, options);
59
- }
60
-
61
- affix(client, req, options);
62
-
63
- req.once('aborted', reject);
64
- req.once('error', reject);
65
- req.once('frameError', reject);
66
- req.once('goaway', reject);
67
- req.once('response', (res) => postflight(req, res, options, {
68
- reject,
69
- resolve,
70
- }));
71
-
72
- dispatch(options, req);
73
- });
74
-
75
- try {
76
- const res = await promise;
77
-
78
- if (digest && !redirected) {
79
- res.body = await res.body();
80
- }
81
-
82
- return res;
83
- } catch (ex) {
84
- const { maxRetryAfter, retry } = options;
85
-
86
- if (retry?.attempts > 0) {
87
- if (retry.errorCodes?.includes(ex.code) || retry.statusCodes?.includes(ex.statusCode)) {
88
- let { interval } = retry;
89
-
90
- if (retry.retryAfter && ex.headers?.[HTTP2_HEADER_RETRY_AFTER]) {
91
- interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
92
- interval = Number(interval) * 1e3 || new Date(interval) - Date.now();
93
- if (interval > maxRetryAfter) {
94
- throw maxRetryAfterError(interval, { cause: ex });
95
- }
96
- } else {
97
- interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
98
- }
99
-
100
- retry.attempts--;
101
- retry.interval = interval;
102
-
103
- return setTimeoutPromise(interval).then(() => overact(url, options));
104
- }
105
- }
106
-
107
- if (digest && !redirected && ex.body) {
108
- ex.body = await ex.body();
109
- }
110
-
111
- if (!thenable) {
112
- throw ex;
113
- } else {
114
- return ex;
115
- }
116
- }
117
- };
1
+ import http from 'node:http';
2
+ import http2 from 'node:http2';
3
+ import https from 'node:https';
4
+ import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
5
+ import { ackn } from './ackn.mjs';
6
+ import { RequestError } from './errors.mjs';
7
+ import { postflight } from './postflight.mjs';
8
+ import { preflight } from './preflight.mjs';
9
+ import { transform } from './transform.mjs';
10
+ import {
11
+ affix,
12
+ dispatch,
13
+ maxRetryAfterError,
14
+ } from './utils.mjs';
15
+
16
+ const {
17
+ HTTP2_HEADER_RETRY_AFTER,
18
+ } = http2.constants;
19
+
20
+ export const transfer = async (options, overact) => {
21
+ const { digest, redirected, thenable, url } = options;
22
+
23
+ if (options.follow === 0) {
24
+ throw new RequestError(`Maximum redirect reached at: ${ url.href }`);
25
+ }
26
+
27
+ if (url.protocol === 'https:') {
28
+ options = !options.h2 ? await ackn(options) : {
29
+ ...options,
30
+ createConnection: null,
31
+ protocol: url.protocol,
32
+ };
33
+ } else if (Reflect.has(options, 'alpnProtocol')) {
34
+ for (const it of [
35
+ 'alpnProtocol',
36
+ 'createConnection',
37
+ 'h2',
38
+ 'protocol',
39
+ ]) { Reflect.deleteProperty(options, it); }
40
+ }
41
+
42
+ try {
43
+ options = await transform(preflight(options));
44
+ } catch (ex) {
45
+ options.createConnection?.().destroy();
46
+ throw ex;
47
+ }
48
+
49
+ const promise = new Promise((resolve, reject) => {
50
+ let client, req;
51
+
52
+ if (options.h2) {
53
+ client = http2.connect(url.origin, options);
54
+ req = client.request(options.headers, options);
55
+ } else {
56
+ const { request } = url.protocol === 'http:' ? http : https;
57
+
58
+ req = request(url, options);
59
+ }
60
+
61
+ affix(client, req, options);
62
+
63
+ req.once('aborted', reject);
64
+ req.once('error', reject);
65
+ req.once('frameError', reject);
66
+ req.once('goaway', reject);
67
+ req.once('response', (res) => postflight(req, res, options, {
68
+ reject,
69
+ resolve,
70
+ }));
71
+
72
+ dispatch(options, req);
73
+ });
74
+
75
+ try {
76
+ const res = await promise;
77
+
78
+ if (digest && !redirected) {
79
+ res.body = await res.body();
80
+ }
81
+
82
+ return res;
83
+ } catch (ex) {
84
+ const { maxRetryAfter, retry } = options;
85
+
86
+ if (retry?.attempts > 0) {
87
+ if (retry.errorCodes?.includes(ex.code) || retry.statusCodes?.includes(ex.statusCode)) {
88
+ let { interval } = retry;
89
+
90
+ if (retry.retryAfter && ex.headers?.[HTTP2_HEADER_RETRY_AFTER]) {
91
+ interval = ex.headers[HTTP2_HEADER_RETRY_AFTER];
92
+ interval = Number(interval) * 1e3 || new Date(interval) - Date.now();
93
+ if (interval > maxRetryAfter) {
94
+ throw maxRetryAfterError(interval, { cause: ex });
95
+ }
96
+ } else {
97
+ interval = new Function('interval', `return Math.ceil(${ retry.backoffStrategy });`)(interval);
98
+ }
99
+
100
+ retry.attempts--;
101
+ retry.interval = interval;
102
+
103
+ return setTimeoutPromise(interval).then(() => overact(url, options));
104
+ }
105
+ }
106
+
107
+ if (digest && !redirected && ex.body) {
108
+ ex.body = await ex.body();
109
+ }
110
+
111
+ if (!thenable) {
112
+ throw ex;
113
+ } else {
114
+ return ex;
115
+ }
116
+ }
117
+ };
package/src/utils.mjs CHANGED
@@ -1,201 +1,201 @@
1
- import {
2
- Blob,
3
- File,
4
- } from 'node:buffer';
5
- import http2 from 'node:http2';
6
- import { pipeline } from 'node:stream';
7
- import zlib from 'node:zlib';
8
- import defaults from './defaults.mjs';
9
- import {
10
- RequestError,
11
- TimeoutError,
12
- } from './errors.mjs';
13
-
14
- const {
15
- HTTP2_HEADER_RETRY_AFTER,
16
- HTTP2_HEADER_STATUS,
17
- } = http2.constants;
18
-
19
- export const admix = (res, headers, options) => {
20
- const { h2 } = options;
21
-
22
- if (h2) {
23
- Reflect.defineProperty(res, 'headers', {
24
- enumerable: true,
25
- value: headers,
26
- });
27
-
28
- Reflect.defineProperty(res, 'httpVersion', {
29
- enumerable: true,
30
- value: `${ h2 + 1 }.0`,
31
- });
32
-
33
- Reflect.defineProperty(res, 'statusCode', {
34
- enumerable: true,
35
- value: headers[HTTP2_HEADER_STATUS],
36
- });
37
- }
38
-
39
- Reflect.defineProperty(res, 'ok', {
40
- enumerable: true,
41
- value: /^2\d{2}$/.test(res.statusCode),
42
- });
43
-
44
- Reflect.defineProperty(res, 'redirected', {
45
- enumerable: true,
46
- value: !!options.redirected,
47
- });
48
- };
49
-
50
- export const affix = (client, req, options) => {
51
- req.once('close', () => client?.close());
52
- req.once('end', () => client?.close());
53
- req.once('timeout', () => req.destroy(new TimeoutError(`Timed out after ${ options.timeout } ms.`)));
54
- req.once('trailers', (trailers) => {
55
- Reflect.defineProperty(req, 'trailers', {
56
- enumerable: true,
57
- value: trailers,
58
- });
59
- });
60
- };
61
-
62
- export const brandCheck = (value, ctor) => {
63
- if (!(value instanceof ctor)) {
64
- throw new TypeError('Illegal invocation');
65
- }
66
- };
67
-
68
- export const compress = (readable, encodings = '') => {
69
- const encoders = [];
70
-
71
- encodings = unwind(encodings);
72
-
73
- for (const encoding of encodings) {
74
- if (/\bbr\b/i.test(encoding)) {
75
- encoders.push(zlib.createBrotliCompress());
76
- } else if (/\bdeflate(?!-(?:\w+)?)\b/i.test(encoding)) {
77
- encoders.push(zlib.createDeflate());
78
- } else if (/\bdeflate-raw\b/i.test(encoding)) {
79
- encoders.push(zlib.createDeflateRaw());
80
- } else if (/\bgzip\b/i.test(encoding)) {
81
- encoders.push(zlib.createGzip());
82
- } else {
83
- return readable;
84
- }
85
- }
86
-
87
- return pipeline(readable, ...encoders, () => void 0);
88
- };
89
-
90
- export const copyWithMerge = (target, ...rest) => {
91
- target = structuredClone(target);
92
- if (!rest.length) {
93
- return target;
94
- }
95
-
96
- return merge(target, ...rest);
97
- };
98
-
99
- export const decompress = (readable, encodings = '') => {
100
- const decoders = [];
101
-
102
- encodings = unwind(encodings);
103
-
104
- for (const encoding of encodings) {
105
- if (/\bbr\b/i.test(encoding)) {
106
- decoders.push(zlib.createBrotliDecompress());
107
- } else if (/\bdeflate(?!-(?:\w+)?)\b/i.test(encoding)) {
108
- decoders.push(zlib.createInflate());
109
- } else if (/\bdeflate-raw\b/i.test(encoding)) {
110
- decoders.push(zlib.createInflateRaw());
111
- } else if (/\bgzip\b/i.test(encoding)) {
112
- decoders.push(zlib.createGunzip());
113
- } else {
114
- return readable;
115
- }
116
- }
117
-
118
- return pipeline(readable, ...decoders, () => void 0);
119
- };
120
-
121
- export const dispatch = ({ body }, req) => {
122
- if (body?.pipe?.constructor === Function) {
123
- body.pipe(req);
124
- } else {
125
- req.end(body);
126
- }
127
- };
128
-
129
- export const isFileLike = (instance) => {
130
- return [
131
- Blob.name,
132
- File.name,
133
- ].includes(instance?.[Symbol.toStringTag]);
134
- };
135
-
136
- export const isReadableStream = (instance) => {
137
- return ReadableStream.name === instance?.[Symbol.toStringTag];
138
- };
139
-
140
- export const maxRetryAfter = Symbol('maxRetryAfter');
141
-
142
- export const maxRetryAfterError = (
143
- interval,
144
- options,
145
- ) => new RequestError(`Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms.`, options);
146
-
147
- export const merge = (target, ...rest) => {
148
- rest = rest.filter((it) => it === Object(it));
149
- for (const source of rest) {
150
- for (const key of Object.getOwnPropertyNames(source)) {
151
- const sv = source[key];
152
- const tv = target[key];
153
-
154
- if (Object(sv) === sv && Object(tv) === tv) {
155
- target[key] = merge(tv, sv);
156
- continue;
157
- }
158
-
159
- target[key] = source[key];
160
- }
161
- }
162
-
163
- return target;
164
- };
165
-
166
- export const normalize = (url, options = {}) => {
167
- if (!options.redirected) {
168
- options = copyWithMerge(defaults.stash, options);
169
- }
170
-
171
- if (options.trimTrailingSlashes) {
172
- url = `${ url }`.replace(/(?<!:)\/+/g, '/');
173
- }
174
-
175
- if (options.stripTrailingSlash) {
176
- url = `${ url }`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
177
- }
178
-
179
- url = new URL(url, options.baseURL);
180
-
181
- return Object.assign(options, { url });
182
- };
183
-
184
- export const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
185
-
186
- export async function* tap(value) {
187
- if (Reflect.has(value, Symbol.asyncIterator)) {
188
- yield* value;
189
- } else if (value.stream) {
190
- yield* value.stream();
191
- } else {
192
- yield await value.arrayBuffer();
193
- }
194
- }
195
-
196
- export const toCamelCase = (str) => str?.toLowerCase().replace(
197
- /\p{Punctuation}.|\p{White_Space}./gu,
198
- (val) => val.replace(/\p{Punctuation}+|\p{White_Space}+/gu, '').toUpperCase(),
199
- );
200
-
201
- export const unwind = (encodings) => encodings.split(',').map((it) => it.trim());
1
+ import {
2
+ Blob,
3
+ File,
4
+ } from 'node:buffer';
5
+ import http2 from 'node:http2';
6
+ import { pipeline } from 'node:stream';
7
+ import zlib from 'node:zlib';
8
+ import defaults from './defaults.mjs';
9
+ import {
10
+ RequestError,
11
+ TimeoutError,
12
+ } from './errors.mjs';
13
+
14
+ const {
15
+ HTTP2_HEADER_RETRY_AFTER,
16
+ HTTP2_HEADER_STATUS,
17
+ } = http2.constants;
18
+
19
+ export const admix = (res, headers, options) => {
20
+ const { h2 } = options;
21
+
22
+ if (h2) {
23
+ Reflect.defineProperty(res, 'headers', {
24
+ enumerable: true,
25
+ value: headers,
26
+ });
27
+
28
+ Reflect.defineProperty(res, 'httpVersion', {
29
+ enumerable: true,
30
+ value: `${ h2 + 1 }.0`,
31
+ });
32
+
33
+ Reflect.defineProperty(res, 'statusCode', {
34
+ enumerable: true,
35
+ value: headers[HTTP2_HEADER_STATUS],
36
+ });
37
+ }
38
+
39
+ Reflect.defineProperty(res, 'ok', {
40
+ enumerable: true,
41
+ value: /^2\d{2}$/.test(res.statusCode),
42
+ });
43
+
44
+ Reflect.defineProperty(res, 'redirected', {
45
+ enumerable: true,
46
+ value: !!options.redirected,
47
+ });
48
+ };
49
+
50
+ export const affix = (client, req, options) => {
51
+ req.once('close', () => client?.close());
52
+ req.once('end', () => client?.close());
53
+ req.once('timeout', () => req.destroy(new TimeoutError(`Timed out after ${ options.timeout } ms.`)));
54
+ req.once('trailers', (trailers) => {
55
+ Reflect.defineProperty(req, 'trailers', {
56
+ enumerable: true,
57
+ value: trailers,
58
+ });
59
+ });
60
+ };
61
+
62
+ export const brandCheck = (value, ctor) => {
63
+ if (!(value instanceof ctor)) {
64
+ throw new TypeError('Illegal invocation');
65
+ }
66
+ };
67
+
68
+ export const compress = (readable, encodings = '') => {
69
+ const encoders = [];
70
+
71
+ encodings = unwind(encodings);
72
+
73
+ for (const encoding of encodings) {
74
+ if (/\bbr\b/i.test(encoding)) {
75
+ encoders.push(zlib.createBrotliCompress());
76
+ } else if (/\bdeflate(?!-(?:\w+)?)\b/i.test(encoding)) {
77
+ encoders.push(zlib.createDeflate());
78
+ } else if (/\bdeflate-raw\b/i.test(encoding)) {
79
+ encoders.push(zlib.createDeflateRaw());
80
+ } else if (/\bgzip\b/i.test(encoding)) {
81
+ encoders.push(zlib.createGzip());
82
+ } else {
83
+ return readable;
84
+ }
85
+ }
86
+
87
+ return pipeline(readable, ...encoders, () => void 0);
88
+ };
89
+
90
+ export const copyWithMerge = (target, ...rest) => {
91
+ target = structuredClone(target);
92
+ if (!rest.length) {
93
+ return target;
94
+ }
95
+
96
+ return merge(target, ...rest);
97
+ };
98
+
99
+ export const decompress = (readable, encodings = '') => {
100
+ const decoders = [];
101
+
102
+ encodings = unwind(encodings);
103
+
104
+ for (const encoding of encodings) {
105
+ if (/\bbr\b/i.test(encoding)) {
106
+ decoders.push(zlib.createBrotliDecompress());
107
+ } else if (/\bdeflate(?!-(?:\w+)?)\b/i.test(encoding)) {
108
+ decoders.push(zlib.createInflate());
109
+ } else if (/\bdeflate-raw\b/i.test(encoding)) {
110
+ decoders.push(zlib.createInflateRaw());
111
+ } else if (/\bgzip\b/i.test(encoding)) {
112
+ decoders.push(zlib.createGunzip());
113
+ } else {
114
+ return readable;
115
+ }
116
+ }
117
+
118
+ return pipeline(readable, ...decoders, () => void 0);
119
+ };
120
+
121
+ export const dispatch = ({ body }, req) => {
122
+ if (body?.pipe?.constructor === Function) {
123
+ body.pipe(req);
124
+ } else {
125
+ req.end(body);
126
+ }
127
+ };
128
+
129
+ export const isFileLike = (instance) => {
130
+ return [
131
+ Blob.name,
132
+ File.name,
133
+ ].includes(instance?.[Symbol.toStringTag]);
134
+ };
135
+
136
+ export const isReadableStream = (instance) => {
137
+ return ReadableStream.name === instance?.[Symbol.toStringTag];
138
+ };
139
+
140
+ export const maxRetryAfter = Symbol('maxRetryAfter');
141
+
142
+ export const maxRetryAfterError = (
143
+ interval,
144
+ options,
145
+ ) => new RequestError(`Maximum '${ HTTP2_HEADER_RETRY_AFTER }' limit exceeded: ${ interval } ms.`, options);
146
+
147
+ export const merge = (target, ...rest) => {
148
+ rest = rest.filter((it) => it === Object(it));
149
+ for (const source of rest) {
150
+ for (const key of Object.getOwnPropertyNames(source)) {
151
+ const sv = source[key];
152
+ const tv = target[key];
153
+
154
+ if (Object(sv) === sv && Object(tv) === tv) {
155
+ target[key] = merge(tv, sv);
156
+ continue;
157
+ }
158
+
159
+ target[key] = source[key];
160
+ }
161
+ }
162
+
163
+ return target;
164
+ };
165
+
166
+ export const normalize = (url, options = {}) => {
167
+ if (!options.redirected) {
168
+ options = copyWithMerge(defaults.stash, options);
169
+ }
170
+
171
+ if (options.trimTrailingSlashes) {
172
+ url = `${ url }`.replace(/(?<!:)\/+/g, '/');
173
+ }
174
+
175
+ if (options.stripTrailingSlash) {
176
+ url = `${ url }`.replace(/\/$|\/(?=#)|\/(?=\?)/g, '');
177
+ }
178
+
179
+ url = new URL(url, options.baseURL);
180
+
181
+ return Object.assign(options, { url });
182
+ };
183
+
184
+ export const sameOrigin = (a, b) => a.protocol === b.protocol && a.hostname === b.hostname && a.port === b.port;
185
+
186
+ export async function* tap(value) {
187
+ if (Reflect.has(value, Symbol.asyncIterator)) {
188
+ yield* value;
189
+ } else if (value.stream) {
190
+ yield* value.stream();
191
+ } else {
192
+ yield await value.arrayBuffer();
193
+ }
194
+ }
195
+
196
+ export const toCamelCase = (str) => str?.toLowerCase().replace(
197
+ /\p{Punctuation}.|\p{White_Space}./gu,
198
+ (val) => val.replace(/\p{Punctuation}+|\p{White_Space}+/gu, '').toUpperCase(),
199
+ );
200
+
201
+ export const unwind = (encodings) => encodings.split(',').map((it) => it.trim());