rekwest 2.3.6 → 2.4.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
@@ -130,7 +130,7 @@ console.log(res.body);
130
130
  * `h2` **{boolean}** `Default: false` Forces use of the HTTP2 protocol
131
131
  * `headers` **{Object}** Headers to add to the request
132
132
  * `parse` **{boolean}** `Default: true` Parse response body, or simply return a buffer
133
- * `redirect` **{boolean | error | follow}** `Default: 'follow'` Controls redirect flow
133
+ * `redirect` **{error | follow | manual}** `Default: 'follow'` Controls redirect flow
134
134
  * `thenable` **{boolean}** `Default: false` Controls promise resolutions
135
135
  * **Returns:** Promise that resolves to
136
136
  extended [http.IncomingMessage](https://nodejs.org/api/http.html#http_class_http_incomingmessage)
@@ -168,6 +168,7 @@ Method with limited functionality to use with streams and pipes
168
168
  * No automata
169
169
  * No redirects
170
170
  * Pass `h2: true` in options to use the HTTP2 protocol
171
+ * Or use `ackn({ url: URL })` method in advance to probe the available protocols
171
172
 
172
173
  ---
173
174
 
package/dist/helpers.js CHANGED
@@ -1,7 +1,7 @@
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.premix = exports.preflight = exports.merge = exports.dispatch = exports.decompress = exports.compress = void 0;
5
5
  exports.tap = tap;
6
6
  exports.transform = void 0;
7
7
 
@@ -179,7 +179,13 @@ const preflight = options => {
179
179
  };
180
180
  options.method ??= method;
181
181
  options.parse ??= true;
182
- options.redirect ??= 'follow';
182
+ options.redirect ??= redirects.follow;
183
+
184
+ if (!Object.values(redirects).includes(options.redirect)) {
185
+ options.createConnection?.().destroy();
186
+ throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${options.redirect}' is not a valid enum value.`);
187
+ }
188
+
183
189
  options.redirected ??= false;
184
190
  options.thenable ??= false;
185
191
  return options;
@@ -230,7 +236,7 @@ const premix = (res, {
230
236
  enumerable: true,
231
237
  value: async function () {
232
238
  if (this.bodyUsed) {
233
- throw new TypeError('Response stream already read');
239
+ throw new TypeError('Response stream already read.');
234
240
  }
235
241
 
236
242
  let spool = [];
@@ -276,6 +282,12 @@ const premix = (res, {
276
282
  };
277
283
 
278
284
  exports.premix = premix;
285
+ const redirects = {
286
+ error: 'error',
287
+ follow: 'follow',
288
+ manual: 'manual'
289
+ };
290
+ exports.redirects = redirects;
279
291
 
280
292
  async function* tap(value) {
281
293
  if (Reflect.has(value, Symbol.asyncIterator)) {
package/dist/index.js CHANGED
@@ -96,7 +96,7 @@ async function rekwest(url, options = {}) {
96
96
  }
97
97
 
98
98
  if (options.body && [HTTP2_METHOD_GET, HTTP2_METHOD_HEAD].includes(options.method)) {
99
- throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body`);
99
+ throw new TypeError(`Request with ${HTTP2_METHOD_GET}/${HTTP2_METHOD_HEAD} method cannot have body.`);
100
100
  }
101
101
 
102
102
  if (!options.follow) {
@@ -175,15 +175,15 @@ async function rekwest(url, options = {}) {
175
175
  });
176
176
 
177
177
  if (follow && /^3\d{2}$/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
178
- if (redirect === 'error') {
179
- res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'`));
178
+ if (redirect === _helpers.redirects.error) {
179
+ res.emit('error', new _errors.RequestError(`Unexpected redirect, redirect mode is set to '${redirect}'.`));
180
180
  }
181
181
 
182
- if (redirect === 'follow') {
182
+ if (redirect === _helpers.redirects.follow) {
183
183
  options.url = new URL(res.headers[HTTP2_HEADER_LOCATION], url).href;
184
184
 
185
185
  if (res.statusCode !== HTTP_STATUS_SEE_OTHER && body === Object(body) && body.pipe?.constructor === Function) {
186
- res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with body as readable stream`));
186
+ res.emit('error', new _errors.RequestError(`Unable to ${redirect} redirect with body as readable stream.`));
187
187
  }
188
188
 
189
189
  options.follow--;
@@ -262,7 +262,8 @@ Reflect.defineProperty(rekwest, 'stream', {
262
262
  headers: {
263
263
  [HTTP2_HEADER_CONTENT_TYPE]: _mediatypes.APPLICATION_OCTET_STREAM
264
264
  }
265
- }, options)
265
+ }, options),
266
+ redirect: _helpers.redirects.manual
266
267
  });
267
268
 
268
269
  if (options.h2) {
package/package.json CHANGED
@@ -13,8 +13,8 @@
13
13
  "@babel/eslint-parser": "^7.16.5",
14
14
  "@babel/preset-env": "^7.16.11",
15
15
  "c8": "^7.11.0",
16
- "eslint": "^8.7.0",
17
- "eslint-config-ultra-refined": "^2.3.0",
16
+ "eslint": "^8.8.0",
17
+ "eslint-config-ultra-refined": "^2.4.0",
18
18
  "mocha": "^9.2.0"
19
19
  },
20
20
  "description": "The robust request library that humanity deserves 🌐",
@@ -56,8 +56,9 @@
56
56
  "lint": "eslint . --ext .cjs,.js,.mjs",
57
57
  "prepack": "npm run build && sh pony.sh",
58
58
  "pretest": "rm -rf coverage && npm run cert:gen",
59
- "test": "mocha --exit --recursive",
59
+ "test": "mocha",
60
+ "test:bail": "mocha --bail",
60
61
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
61
62
  },
62
- "version": "2.3.6"
63
+ "version": "2.4.0"
63
64
  }
package/src/helpers.mjs CHANGED
@@ -1,317 +1,331 @@
1
- import { Blob } from 'buffer';
2
- import http2 from 'http2';
3
- import {
4
- PassThrough,
5
- Readable,
6
- } from 'stream';
7
- import {
8
- promisify,
9
- types,
10
- } from 'util';
11
- import zlib from 'zlib';
12
- import { Cookies } from './cookies.mjs';
13
- import { File } from './file.mjs';
14
- import { FormData } from './formdata.mjs';
15
- import {
16
- APPLICATION_FORM_URLENCODED,
17
- APPLICATION_JSON,
18
- APPLICATION_OCTET_STREAM,
19
- TEXT_PLAIN,
20
- WILDCARD,
21
- } from './mediatypes.mjs';
22
-
23
- const {
24
- HTTP2_HEADER_ACCEPT,
25
- HTTP2_HEADER_ACCEPT_ENCODING,
26
- HTTP2_HEADER_AUTHORITY,
27
- HTTP2_HEADER_CONTENT_ENCODING,
28
- HTTP2_HEADER_CONTENT_LENGTH,
29
- HTTP2_HEADER_CONTENT_TYPE,
30
- HTTP2_HEADER_COOKIE,
31
- HTTP2_HEADER_METHOD,
32
- HTTP2_HEADER_PATH,
33
- HTTP2_HEADER_SCHEME,
34
- HTTP2_METHOD_GET,
35
- HTTP2_METHOD_HEAD,
36
- } = http2.constants;
37
-
38
- const brotliCompress = promisify(zlib.brotliCompress);
39
- const brotliDecompress = promisify(zlib.brotliDecompress);
40
- const gzip = promisify(zlib.gzip);
41
- const gunzip = promisify(zlib.gunzip);
42
- const deflate = promisify(zlib.deflate);
43
- const inflate = promisify(zlib.inflate);
44
-
45
- export const compress = (buf, encoding, { async = false } = {}) => {
46
- encoding &&= encoding.match(/(?<encoding>\bbr\b|\bdeflate\b|\bgzip\b)/i)?.groups.encoding.toLowerCase();
47
- const compressor = {
48
- br: async ? brotliCompress : zlib.brotliCompressSync,
49
- deflate: async ? deflate : zlib.deflateSync,
50
- gzip: async ? gzip : zlib.gzipSync,
51
- }[encoding];
52
-
53
- return compressor?.(buf) ?? (async ? Promise.resolve(buf) : buf);
54
- };
55
-
56
- export const decompress = (buf, encoding, { async = false } = {}) => {
57
- encoding &&= encoding.match(/(?<encoding>\bbr\b|\bdeflate\b|\bgzip\b)/i)?.groups.encoding.toLowerCase();
58
- const decompressor = {
59
- br: async ? brotliDecompress : zlib.brotliDecompressSync,
60
- deflate: async ? inflate : zlib.inflateSync,
61
- gzip: async ? gunzip : zlib.gunzipSync,
62
- }[encoding];
63
-
64
- return decompressor?.(buf) ?? (async ? Promise.resolve(buf) : buf);
65
- };
66
-
67
- export const dispatch = (req, { body, headers }) => {
68
- if (types.isUint8Array(body)) {
69
- return req.end(body);
70
- }
71
-
72
- if (body === Object(body) && !Buffer.isBuffer(body)) {
73
- if (body.pipe?.constructor !== Function
74
- && (Reflect.has(body, Symbol.asyncIterator) || Reflect.has(body, Symbol.iterator))) {
75
- body = Readable.from(body);
76
- }
77
-
78
- const compressor = {
79
- br: zlib.createBrotliCompress,
80
- deflate: zlib.createDeflate,
81
- gzip: zlib.createGzip,
82
- }[headers[HTTP2_HEADER_CONTENT_ENCODING]] ?? PassThrough;
83
-
84
- body.pipe(compressor()).pipe(req);
85
- } else {
86
- req.end(body);
87
- }
88
- };
89
-
90
- export const merge = (target = {}, ...rest) => {
91
- target = JSON.parse(JSON.stringify(target));
92
- if (!rest.length) {
93
- return target;
94
- }
95
-
96
- rest.filter((it) => it === Object(it)).forEach((it) => {
97
- Object.entries(it).reduce((acc, [key, val]) => {
98
- if ([
99
- acc[key]?.constructor,
100
- val?.constructor,
101
- ].every((it) => [
102
- Array,
103
- Object,
104
- ].includes(it))) {
105
- if (acc[key]?.constructor === val.constructor) {
106
- acc[key] = merge(acc[key], val);
107
- } else {
108
- acc[key] = val;
109
- }
110
- } else {
111
- acc[key] = val;
112
- }
113
-
114
- return acc;
115
- }, target);
116
- });
117
-
118
- return target;
119
- };
120
-
121
- export const preflight = (options) => {
122
- const url = options.url = new URL(options.url);
123
- const { cookies, h2 = false, method = HTTP2_METHOD_GET, headers, redirected } = options;
124
-
125
- if (h2) {
126
- options.endStream = [
127
- HTTP2_METHOD_GET,
128
- HTTP2_METHOD_HEAD,
129
- ].includes(method);
130
- }
131
-
132
- if (cookies !== false) {
133
- let cookie = Cookies.jar.get(url.origin);
134
-
135
- if (cookies === Object(cookies) && !redirected) {
136
- if (cookie) {
137
- new Cookies(cookies).forEach(function (val, key) {
138
- this.set(key, val);
139
- }, cookie);
140
- } else {
141
- cookie = new Cookies(cookies);
142
- Cookies.jar.set(url.origin, cookie);
143
- }
144
- }
145
-
146
- options.headers = {
147
- ...cookie && { [HTTP2_HEADER_COOKIE]: cookie },
148
- ...headers,
149
- };
150
- }
151
-
152
- options.digest ??= true;
153
- options.follow ??= 20;
154
- options.h2 ??= h2;
155
- options.headers = {
156
- [HTTP2_HEADER_ACCEPT]: `${ APPLICATION_JSON }, ${ TEXT_PLAIN }, ${ WILDCARD }`,
157
- [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, gzip, identity',
158
- ...Object.entries(options.headers ?? {})
159
- .reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
160
- ...h2 && {
161
- [HTTP2_HEADER_AUTHORITY]: url.host,
162
- [HTTP2_HEADER_METHOD]: method,
163
- [HTTP2_HEADER_PATH]: `${ url.pathname }${ url.search }`,
164
- [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, ''),
165
- },
166
- };
167
-
168
- options.method ??= method;
169
- options.parse ??= true;
170
- options.redirect ??= 'follow';
171
- options.redirected ??= false;
172
- options.thenable ??= false;
173
-
174
- return options;
175
- };
176
-
177
- export const premix = (res, { digest = false, parse = false } = {}) => {
178
- if (!digest) {
179
- Object.defineProperties(res, {
180
- arrayBuffer: {
181
- enumerable: true,
182
- value: function () {
183
- parse &&= false;
184
-
185
- return this.body().then(({ buffer, byteLength, byteOffset }) => buffer.slice(
186
- byteOffset,
187
- byteOffset + byteLength,
188
- ));
189
- },
190
- },
191
- blob: {
192
- enumerable: true,
193
- value: function () {
194
- return this.arrayBuffer().then((res) => new Blob([res]));
195
- },
196
- },
197
- json: {
198
- enumerable: true,
199
- value: function () {
200
- return this.text().then((res) => JSON.parse(res));
201
- },
202
- },
203
- text: {
204
- enumerable: true,
205
- value: function () {
206
- return this.blob().then((blob) => blob.text());
207
- },
208
- },
209
- });
210
- }
211
-
212
- return Object.defineProperties(res, {
213
- body: {
214
- enumerable: true,
215
- value: async function () {
216
- if (this.bodyUsed) {
217
- throw new TypeError('Response stream already read');
218
- }
219
-
220
- let spool = [];
221
-
222
- for await (const chunk of this) {
223
- spool.push(chunk);
224
- }
225
-
226
- spool = Buffer.concat(spool);
227
-
228
- if (spool.length) {
229
- spool = await decompress(spool, this.headers[HTTP2_HEADER_CONTENT_ENCODING], { async: true });
230
- }
231
-
232
- if (spool.length && parse) {
233
- const contentType = this.headers[HTTP2_HEADER_CONTENT_TYPE] ?? '';
234
- const charset = contentType.split(';')
235
- .find((it) => /charset=/i.test(it))
236
- ?.toLowerCase()
237
- .replace('charset=', '')
238
- .replace('iso-8859-1', 'latin1')
239
- .trim() || 'utf-8';
240
-
241
- if (/\bjson\b/i.test(contentType)) {
242
- spool = JSON.parse(spool.toString(charset));
243
- } else if (/\b(text|xml)\b/i.test(contentType)) {
244
- if (/\b(latin1|ucs-2|utf-(8|16le))\b/.test(charset)) {
245
- spool = spool.toString(charset);
246
- } else {
247
- spool = new TextDecoder(charset).decode(spool);
248
- }
249
- }
250
- }
251
-
252
- return spool;
253
- },
254
- writable: true,
255
- },
256
- bodyUsed: {
257
- enumerable: true,
258
- get: function () {
259
- return this.readableEnded;
260
- },
261
- },
262
- });
263
- };
264
-
265
- export async function* tap(value) {
266
- if (Reflect.has(value, Symbol.asyncIterator)) {
267
- yield* value;
268
- } else if (value.stream) {
269
- yield* value.stream();
270
- } else {
271
- yield await value.arrayBuffer();
272
- }
273
- }
274
-
275
- export const transform = (body, options) => {
276
- let headers = {};
277
-
278
- if (File.alike(body)) {
279
- headers = {
280
- [HTTP2_HEADER_CONTENT_LENGTH]: body.size,
281
- [HTTP2_HEADER_CONTENT_TYPE]: body.type || APPLICATION_OCTET_STREAM,
282
- };
283
- body = body.stream?.() ?? Readable.from(tap(body));
284
- } else if (FormData.alike(body)) {
285
- body = FormData.actuate(body);
286
- headers = { [HTTP2_HEADER_CONTENT_TYPE]: body.contentType };
287
- } else if (body === Object(body) && !Reflect.has(body, Symbol.asyncIterator)) {
288
- if (body.constructor === URLSearchParams) {
289
- headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_FORM_URLENCODED };
290
- body = body.toString();
291
- } else if (!Buffer.isBuffer(body)
292
- && !(!Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
293
- headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_JSON };
294
- body = JSON.stringify(body);
295
- }
296
-
297
- if (types.isUint8Array(body) || Buffer.isBuffer(body) || body !== Object(body)) {
298
- if (options.headers[HTTP2_HEADER_CONTENT_ENCODING]) {
299
- body = compress(body, options.headers[HTTP2_HEADER_CONTENT_ENCODING]);
300
- }
301
-
302
- headers = {
303
- ...headers,
304
- [HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body),
305
- };
306
- }
307
- }
308
-
309
- Object.assign(options.headers, {
310
- ...headers,
311
- ...options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
312
- [HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE],
313
- },
314
- });
315
-
316
- return body;
317
- };
1
+ import { Blob } from 'buffer';
2
+ import http2 from 'http2';
3
+ import {
4
+ PassThrough,
5
+ Readable,
6
+ } from 'stream';
7
+ import {
8
+ promisify,
9
+ types,
10
+ } from 'util';
11
+ import zlib from 'zlib';
12
+ import { Cookies } from './cookies.mjs';
13
+ import { File } from './file.mjs';
14
+ import { FormData } from './formdata.mjs';
15
+ import {
16
+ APPLICATION_FORM_URLENCODED,
17
+ APPLICATION_JSON,
18
+ APPLICATION_OCTET_STREAM,
19
+ TEXT_PLAIN,
20
+ WILDCARD,
21
+ } from './mediatypes.mjs';
22
+
23
+ const {
24
+ HTTP2_HEADER_ACCEPT,
25
+ HTTP2_HEADER_ACCEPT_ENCODING,
26
+ HTTP2_HEADER_AUTHORITY,
27
+ HTTP2_HEADER_CONTENT_ENCODING,
28
+ HTTP2_HEADER_CONTENT_LENGTH,
29
+ HTTP2_HEADER_CONTENT_TYPE,
30
+ HTTP2_HEADER_COOKIE,
31
+ HTTP2_HEADER_METHOD,
32
+ HTTP2_HEADER_PATH,
33
+ HTTP2_HEADER_SCHEME,
34
+ HTTP2_METHOD_GET,
35
+ HTTP2_METHOD_HEAD,
36
+ } = http2.constants;
37
+
38
+ const brotliCompress = promisify(zlib.brotliCompress);
39
+ const brotliDecompress = promisify(zlib.brotliDecompress);
40
+ const gzip = promisify(zlib.gzip);
41
+ const gunzip = promisify(zlib.gunzip);
42
+ const deflate = promisify(zlib.deflate);
43
+ const inflate = promisify(zlib.inflate);
44
+
45
+ export const compress = (buf, encoding, { async = false } = {}) => {
46
+ encoding &&= encoding.match(/(?<encoding>\bbr\b|\bdeflate\b|\bgzip\b)/i)?.groups.encoding.toLowerCase();
47
+ const compressor = {
48
+ br: async ? brotliCompress : zlib.brotliCompressSync,
49
+ deflate: async ? deflate : zlib.deflateSync,
50
+ gzip: async ? gzip : zlib.gzipSync,
51
+ }[encoding];
52
+
53
+ return compressor?.(buf) ?? (async ? Promise.resolve(buf) : buf);
54
+ };
55
+
56
+ export const decompress = (buf, encoding, { async = false } = {}) => {
57
+ encoding &&= encoding.match(/(?<encoding>\bbr\b|\bdeflate\b|\bgzip\b)/i)?.groups.encoding.toLowerCase();
58
+ const decompressor = {
59
+ br: async ? brotliDecompress : zlib.brotliDecompressSync,
60
+ deflate: async ? inflate : zlib.inflateSync,
61
+ gzip: async ? gunzip : zlib.gunzipSync,
62
+ }[encoding];
63
+
64
+ return decompressor?.(buf) ?? (async ? Promise.resolve(buf) : buf);
65
+ };
66
+
67
+ export const dispatch = (req, { body, headers }) => {
68
+ if (types.isUint8Array(body)) {
69
+ return req.end(body);
70
+ }
71
+
72
+ if (body === Object(body) && !Buffer.isBuffer(body)) {
73
+ if (body.pipe?.constructor !== Function
74
+ && (Reflect.has(body, Symbol.asyncIterator) || Reflect.has(body, Symbol.iterator))) {
75
+ body = Readable.from(body);
76
+ }
77
+
78
+ const compressor = {
79
+ br: zlib.createBrotliCompress,
80
+ deflate: zlib.createDeflate,
81
+ gzip: zlib.createGzip,
82
+ }[headers[HTTP2_HEADER_CONTENT_ENCODING]] ?? PassThrough;
83
+
84
+ body.pipe(compressor()).pipe(req);
85
+ } else {
86
+ req.end(body);
87
+ }
88
+ };
89
+
90
+ export const merge = (target = {}, ...rest) => {
91
+ target = JSON.parse(JSON.stringify(target));
92
+ if (!rest.length) {
93
+ return target;
94
+ }
95
+
96
+ rest.filter((it) => it === Object(it)).forEach((it) => {
97
+ Object.entries(it).reduce((acc, [key, val]) => {
98
+ if ([
99
+ acc[key]?.constructor,
100
+ val?.constructor,
101
+ ].every((it) => [
102
+ Array,
103
+ Object,
104
+ ].includes(it))) {
105
+ if (acc[key]?.constructor === val.constructor) {
106
+ acc[key] = merge(acc[key], val);
107
+ } else {
108
+ acc[key] = val;
109
+ }
110
+ } else {
111
+ acc[key] = val;
112
+ }
113
+
114
+ return acc;
115
+ }, target);
116
+ });
117
+
118
+ return target;
119
+ };
120
+
121
+ export const preflight = (options) => {
122
+ const url = options.url = new URL(options.url);
123
+ const { cookies, h2 = false, method = HTTP2_METHOD_GET, headers, redirected } = options;
124
+
125
+ if (h2) {
126
+ options.endStream = [
127
+ HTTP2_METHOD_GET,
128
+ HTTP2_METHOD_HEAD,
129
+ ].includes(method);
130
+ }
131
+
132
+ if (cookies !== false) {
133
+ let cookie = Cookies.jar.get(url.origin);
134
+
135
+ if (cookies === Object(cookies) && !redirected) {
136
+ if (cookie) {
137
+ new Cookies(cookies).forEach(function (val, key) {
138
+ this.set(key, val);
139
+ }, cookie);
140
+ } else {
141
+ cookie = new Cookies(cookies);
142
+ Cookies.jar.set(url.origin, cookie);
143
+ }
144
+ }
145
+
146
+ options.headers = {
147
+ ...cookie && { [HTTP2_HEADER_COOKIE]: cookie },
148
+ ...headers,
149
+ };
150
+ }
151
+
152
+ options.digest ??= true;
153
+ options.follow ??= 20;
154
+ options.h2 ??= h2;
155
+ options.headers = {
156
+ [HTTP2_HEADER_ACCEPT]: `${ APPLICATION_JSON }, ${ TEXT_PLAIN }, ${ WILDCARD }`,
157
+ [HTTP2_HEADER_ACCEPT_ENCODING]: 'br, deflate, gzip, identity',
158
+ ...Object.entries(options.headers ?? {})
159
+ .reduce((acc, [key, val]) => (acc[key.toLowerCase()] = val, acc), {}),
160
+ ...h2 && {
161
+ [HTTP2_HEADER_AUTHORITY]: url.host,
162
+ [HTTP2_HEADER_METHOD]: method,
163
+ [HTTP2_HEADER_PATH]: `${ url.pathname }${ url.search }`,
164
+ [HTTP2_HEADER_SCHEME]: url.protocol.replace(/\p{Punctuation}/gu, ''),
165
+ },
166
+ };
167
+
168
+ options.method ??= method;
169
+ options.parse ??= true;
170
+ options.redirect ??= redirects.follow;
171
+
172
+ if (!Object.values(redirects).includes(options.redirect)) {
173
+ options.createConnection?.().destroy();
174
+ throw new TypeError(`Failed to read the 'redirect' property from 'options': The provided value '${
175
+ options.redirect
176
+ }' is not a valid enum value.`);
177
+ }
178
+
179
+ options.redirected ??= false;
180
+ options.thenable ??= false;
181
+
182
+ return options;
183
+ };
184
+
185
+ export const premix = (res, { digest = false, parse = false } = {}) => {
186
+ if (!digest) {
187
+ Object.defineProperties(res, {
188
+ arrayBuffer: {
189
+ enumerable: true,
190
+ value: function () {
191
+ parse &&= false;
192
+
193
+ return this.body().then(({ buffer, byteLength, byteOffset }) => buffer.slice(
194
+ byteOffset,
195
+ byteOffset + byteLength,
196
+ ));
197
+ },
198
+ },
199
+ blob: {
200
+ enumerable: true,
201
+ value: function () {
202
+ return this.arrayBuffer().then((res) => new Blob([res]));
203
+ },
204
+ },
205
+ json: {
206
+ enumerable: true,
207
+ value: function () {
208
+ return this.text().then((res) => JSON.parse(res));
209
+ },
210
+ },
211
+ text: {
212
+ enumerable: true,
213
+ value: function () {
214
+ return this.blob().then((blob) => blob.text());
215
+ },
216
+ },
217
+ });
218
+ }
219
+
220
+ return Object.defineProperties(res, {
221
+ body: {
222
+ enumerable: true,
223
+ value: async function () {
224
+ if (this.bodyUsed) {
225
+ throw new TypeError('Response stream already read.');
226
+ }
227
+
228
+ let spool = [];
229
+
230
+ for await (const chunk of this) {
231
+ spool.push(chunk);
232
+ }
233
+
234
+ spool = Buffer.concat(spool);
235
+
236
+ if (spool.length) {
237
+ spool = await decompress(spool, this.headers[HTTP2_HEADER_CONTENT_ENCODING], { async: true });
238
+ }
239
+
240
+ if (spool.length && parse) {
241
+ const contentType = this.headers[HTTP2_HEADER_CONTENT_TYPE] ?? '';
242
+ const charset = contentType.split(';')
243
+ .find((it) => /charset=/i.test(it))
244
+ ?.toLowerCase()
245
+ .replace('charset=', '')
246
+ .replace('iso-8859-1', 'latin1')
247
+ .trim() || 'utf-8';
248
+
249
+ if (/\bjson\b/i.test(contentType)) {
250
+ spool = JSON.parse(spool.toString(charset));
251
+ } else if (/\b(text|xml)\b/i.test(contentType)) {
252
+ if (/\b(latin1|ucs-2|utf-(8|16le))\b/.test(charset)) {
253
+ spool = spool.toString(charset);
254
+ } else {
255
+ spool = new TextDecoder(charset).decode(spool);
256
+ }
257
+ }
258
+ }
259
+
260
+ return spool;
261
+ },
262
+ writable: true,
263
+ },
264
+ bodyUsed: {
265
+ enumerable: true,
266
+ get: function () {
267
+ return this.readableEnded;
268
+ },
269
+ },
270
+ });
271
+ };
272
+
273
+ export const redirects = {
274
+ error: 'error',
275
+ follow: 'follow',
276
+ manual: 'manual',
277
+ };
278
+
279
+ export async function* tap(value) {
280
+ if (Reflect.has(value, Symbol.asyncIterator)) {
281
+ yield* value;
282
+ } else if (value.stream) {
283
+ yield* value.stream();
284
+ } else {
285
+ yield await value.arrayBuffer();
286
+ }
287
+ }
288
+
289
+ export const transform = (body, options) => {
290
+ let headers = {};
291
+
292
+ if (File.alike(body)) {
293
+ headers = {
294
+ [HTTP2_HEADER_CONTENT_LENGTH]: body.size,
295
+ [HTTP2_HEADER_CONTENT_TYPE]: body.type || APPLICATION_OCTET_STREAM,
296
+ };
297
+ body = body.stream?.() ?? Readable.from(tap(body));
298
+ } else if (FormData.alike(body)) {
299
+ body = FormData.actuate(body);
300
+ headers = { [HTTP2_HEADER_CONTENT_TYPE]: body.contentType };
301
+ } else if (body === Object(body) && !Reflect.has(body, Symbol.asyncIterator)) {
302
+ if (body.constructor === URLSearchParams) {
303
+ headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_FORM_URLENCODED };
304
+ body = body.toString();
305
+ } else if (!Buffer.isBuffer(body)
306
+ && !(!Array.isArray(body) && Reflect.has(body, Symbol.iterator))) {
307
+ headers = { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_JSON };
308
+ body = JSON.stringify(body);
309
+ }
310
+
311
+ if (types.isUint8Array(body) || Buffer.isBuffer(body) || body !== Object(body)) {
312
+ if (options.headers[HTTP2_HEADER_CONTENT_ENCODING]) {
313
+ body = compress(body, options.headers[HTTP2_HEADER_CONTENT_ENCODING]);
314
+ }
315
+
316
+ headers = {
317
+ ...headers,
318
+ [HTTP2_HEADER_CONTENT_LENGTH]: Buffer.byteLength(body),
319
+ };
320
+ }
321
+ }
322
+
323
+ Object.assign(options.headers, {
324
+ ...headers,
325
+ ...options.headers[HTTP2_HEADER_CONTENT_TYPE] && {
326
+ [HTTP2_HEADER_CONTENT_TYPE]: options.headers[HTTP2_HEADER_CONTENT_TYPE],
327
+ },
328
+ });
329
+
330
+ return body;
331
+ };
package/src/index.mjs CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  merge,
10
10
  preflight,
11
11
  premix,
12
+ redirects,
12
13
  transform,
13
14
  } from './helpers.mjs';
14
15
  import { APPLICATION_OCTET_STREAM } from './mediatypes.mjs';
@@ -44,7 +45,7 @@ export default async function rekwest(url, options = {}) {
44
45
  HTTP2_METHOD_GET,
45
46
  HTTP2_METHOD_HEAD,
46
47
  ].includes(options.method)) {
47
- throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body`);
48
+ throw new TypeError(`Request with ${ HTTP2_METHOD_GET }/${ HTTP2_METHOD_HEAD } method cannot have body.`);
48
49
  }
49
50
 
50
51
  if (!options.follow) {
@@ -122,16 +123,16 @@ export default async function rekwest(url, options = {}) {
122
123
  });
123
124
 
124
125
  if (follow && /^3\d{2}$/.test(res.statusCode) && res.headers[HTTP2_HEADER_LOCATION]) {
125
- if (redirect === 'error') {
126
- res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'`));
126
+ if (redirect === redirects.error) {
127
+ res.emit('error', new RequestError(`Unexpected redirect, redirect mode is set to '${ redirect }'.`));
127
128
  }
128
129
 
129
- if (redirect === 'follow') {
130
+ if (redirect === redirects.follow) {
130
131
  options.url = new URL(res.headers[HTTP2_HEADER_LOCATION], url).href;
131
132
 
132
133
  if (res.statusCode !== HTTP_STATUS_SEE_OTHER
133
134
  && body === Object(body) && body.pipe?.constructor === Function) {
134
- res.emit('error', new RequestError(`Unable to ${ redirect } redirect with body as readable stream`));
135
+ res.emit('error', new RequestError(`Unable to ${ redirect } redirect with body as readable stream.`));
135
136
  }
136
137
 
137
138
  options.follow--;
@@ -211,6 +212,7 @@ Reflect.defineProperty(rekwest, 'stream', {
211
212
  ...merge(rekwest.defaults, {
212
213
  headers: { [HTTP2_HEADER_CONTENT_TYPE]: APPLICATION_OCTET_STREAM },
213
214
  }, options),
215
+ redirect: redirects.manual,
214
216
  });
215
217
 
216
218
  if (options.h2) {