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