rekwest 7.2.4 → 7.2.6

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
@@ -22,7 +22,7 @@ and [http2.request](https://nodejs.org/api/http2.html#clienthttp2sessionrequesth
22
22
 
23
23
  ## Prerequisites
24
24
 
25
- * Node.js `>= 20.0.0`
25
+ * Node.js `>=20.0.0`
26
26
 
27
27
  ## Installation
28
28
 
@@ -48,10 +48,11 @@ const res = await rekwest(url, {
48
48
  body: { celestial: 'payload' },
49
49
  headers: {
50
50
  [HTTP2_HEADER_AUTHORIZATION]: 'Bearer [token]',
51
- [HTTP2_HEADER_CONTENT_ENCODING]: 'br', // Enables: body encoding
52
- /** [HTTP2_HEADER_CONTENT_TYPE] is undue for
51
+ [HTTP2_HEADER_CONTENT_ENCODING]: 'br', // Enables: body encoding
52
+ /**
53
+ * [HTTP2_HEADER_CONTENT_TYPE] is undue for
53
54
  * Array/Blob/File/FormData/Object/URLSearchParams body types
54
- * and will be set automatically, with an option to override it here
55
+ * and will be set automatically, with an option to override it here.
55
56
  */
56
57
  },
57
58
  method: HTTP2_METHOD_POST,
@@ -68,8 +69,6 @@ console.log(res.body);
68
69
  import { Readable } from 'node:stream';
69
70
  import rekwest, {
70
71
  constants,
71
- Blob,
72
- File,
73
72
  FormData,
74
73
  } from 'rekwest';
75
74
 
@@ -82,16 +81,21 @@ const {
82
81
 
83
82
  const blob = new Blob(['bits']);
84
83
  const file = new File(['bits'], 'file.xyz');
85
- const readable = Readable.from('bits');
84
+ const rbl = Readable.from('bits');
85
+ const rds = ReadableStream.from('bits');
86
86
 
87
87
  const fd = new FormData({
88
- aux: Date.now(), // Either [[key, value]] or kv sequenceable
88
+ aux: new Date(), // Either [[key, value]] or kv sequenceable
89
89
  });
90
90
 
91
91
  fd.append('celestial', 'payload');
92
92
  fd.append('blob', blob, 'blob.xyz');
93
93
  fd.append('file', file);
94
- fd.append('readable', readable, 'readable.xyz');
94
+ fd.append('rbl', rbl, 'rbl.xyz');
95
+ fd.append('rds', rds, 'rds.xyz');
96
+ /**
97
+ * Streamable entries are consumed on request submittion.
98
+ */
95
99
 
96
100
  const url = 'https://somewhe.re/somewhat/endpoint';
97
101
 
@@ -99,7 +103,7 @@ const res = await rekwest(url, {
99
103
  body: fd,
100
104
  headers: {
101
105
  [HTTP2_HEADER_AUTHORIZATION]: 'Bearer [token]',
102
- [HTTP2_HEADER_CONTENT_ENCODING]: 'zstd', // Enables: body encoding
106
+ [HTTP2_HEADER_CONTENT_ENCODING]: 'zstd', // Enables: body encoding
103
107
  },
104
108
  method: HTTP2_METHOD_POST,
105
109
  });
@@ -113,70 +117,70 @@ console.log(res.body);
113
117
 
114
118
  #### `rekwest(url[, options])`
115
119
 
116
- * `url` **{string | URL}** The URL to send the request to
120
+ * `url` **{string | URL}** The URL to send the request to.
117
121
  * `options` **{Object}**
118
122
  Extends [http(s).RequestOptions](https://nodejs.org/api/https.html#httpsrequesturl-options-callback) along with
119
123
  extra [http2.ClientSessionOptions](https://nodejs.org/api/http2.html#http2connectauthority-options-listener)
120
124
  & [http2.ClientSessionRequestOptions](https://nodejs.org/api/http2.html#clienthttp2sessionrequestheaders-options)
121
125
  and [tls.ConnectionOptions](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)
122
- for HTTP/2 attunes
123
- * `allowDowngrade` **{boolean}** `Default: false` Controls whether `https:` redirects to `http:` are allowed
124
- * `baseURL` **{string | URL}** The base URL to use in cases where `url` is a relative URL
126
+ for HTTP/2 attunes.
127
+ * `allowDowngrade` **{boolean}** `Default: false` Controls whether `https:` redirects to `http:` are allowed.
128
+ * `baseURL` **{string | URL}** The base URL to use in cases where `url` is a relative URL.
125
129
  * `body` **{string | Array | ArrayBuffer | ArrayBufferView | AsyncIterator | Blob | Buffer | DataView | File |
126
130
  FormData | Iterator | Object | Readable | ReadableStream | SharedArrayBuffer | URLSearchParams}** The body to send
127
- with the request
131
+ with the request.
128
132
  * `bufferBody` **{boolean}** `Default: false` Toggles the buffering of the streamable request bodies for redirects and
129
- retries
133
+ retries.
130
134
  * `cookies` **{boolean | string | string[] | [k, v][] | Cookies | Object | URLSearchParams}** `Default: true` The
131
135
  cookies to add to the request. Manually set `cookie` header to override.
132
- * `cookiesTTL` **{boolean}** `Default: false` Controls enablement of TTL for the cookies cache
136
+ * `cookiesTTL` **{boolean}** `Default: false` Controls enablement of TTL for the cookies cache.
133
137
  * `credentials` **{include | omit | same-origin}** `Default: same-origin` Controls credentials in case of cross-origin
134
- redirects
135
- * `decodersOptions` **{Object}** Configures decoders options, e.g.: `brotli`, `zlib`, `zstd`
136
- * `digest` **{boolean}** `Default: true` Controls whether to read the response stream or add a mixin
137
- * `encodersOptions` **{Object}** Configures encoders options, e.g.: `brotli`, `zlib`, `zstd`
138
- * `follow` **{number}** `Default: 20` The number of redirects to follow
139
- * `h2` **{boolean}** `Default: false` Forces the use of HTTP/2 protocol
140
- * `headers` **{Object}** The headers to add to the request
141
- * `params` **{Object}** The search params to add to the `url`
142
- * `parse` **{boolean}** `Default: true` Controls whether to parse response body or return a buffer
143
- * `redirect` **{error | follow | manual}** `Default: follow` Controls the redirect flows
144
- * `retry` **{Object}** Represents the retry options
145
- * `attempts` **{number}** `Default: 0` The number of retry attempts
138
+ redirects.
139
+ * `decodersOptions` **{Object}** Configures decoders options, e.g.: `brotli`, `zlib`, `zstd`.
140
+ * `digest` **{boolean}** `Default: true` Controls whether to read the response stream or add a mixin.
141
+ * `encodersOptions` **{Object}** Configures encoders options, e.g.: `brotli`, `zlib`, `zstd`.
142
+ * `follow` **{number}** `Default: 20` The number of redirects to follow.
143
+ * `h2` **{boolean}** `Default: false` Forces the use of HTTP/2 protocol.
144
+ * `headers` **{Object}** The headers to add to the request.
145
+ * `params` **{Object}** The search params to add to the `url`.
146
+ * `parse` **{boolean}** `Default: true` Controls whether to parse response body or return a buffer.
147
+ * `redirect` **{error | follow | manual}** `Default: follow` Controls the redirect flows.
148
+ * `retry` **{Object}** Represents the retry options.
149
+ * `attempts` **{number}** `Default: 0` The number of retry attempts.
146
150
  * `backoffStrategy` **{string}** `Default: interval * Math.log(Math.random() * (Math.E * Math.E - Math.E) + Math.E)`
147
151
  The backoff strategy uses a log-uniform algorithm. To fix the interval, set the value to `interval * 1`.
148
152
  * `errorCodes` **{string[]}**
149
153
  `Default: ['ECONNREFUSED', 'ECONNRESET', 'EHOSTDOWN', 'EHOSTUNREACH', 'ENETDOWN', 'ENETUNREACH', 'ENOTFOUND', 'ERR_HTTP2_STREAM_ERROR']`
150
- The list of error codes to retry on
151
- * `interval` **{number}** `Default: 1e3` The initial retry interval
152
- * `maxRetryAfter` **{number}** `Default: 3e5` The maximum `retry-after` limit in milliseconds
153
- * `retryAfter` **{boolean}** `Default: true` Controls `retry-after` header receptiveness
154
- * `statusCodes` **{number[]}** `Default: [429, 500, 502, 503, 504]` The list of status codes to retry on
155
- * `stripTrailingSlash` **{boolean}** `Default: false` Controls whether to strip trailing slash at the end of the URL
156
- * `thenable` **{boolean}** `Default: false` Controls the promise resolutions
157
- * `timeout` **{number}** `Default: 3e5` The number of milliseconds a request can take before termination
158
- * `trimTrailingSlashes` **{boolean}** `Default: false` Controls whether to trim trailing slashes within the URL
154
+ The list of error codes to retry on.
155
+ * `interval` **{number}** `Default: 1e3` The initial retry interval.
156
+ * `maxRetryAfter` **{number}** `Default: 3e5` The maximum `retry-after` limit in milliseconds.
157
+ * `retryAfter` **{boolean}** `Default: true` Controls `retry-after` header receptiveness.
158
+ * `statusCodes` **{number[]}** `Default: [429, 500, 502, 503, 504]` The list of status codes to retry on.
159
+ * `stripTrailingSlash` **{boolean}** `Default: false` Controls whether to strip trailing slash at the end of the URL.
160
+ * `thenable` **{boolean}** `Default: false` Controls the promise resolutions.
161
+ * `timeout` **{number}** `Default: 3e5` The number of milliseconds a request can take before termination.
162
+ * `trimTrailingSlashes` **{boolean}** `Default: false` Controls whether to trim trailing slashes within the URL.
159
163
  * **Returns:** Promise that resolves to
160
164
  extended [http.IncomingMessage](https://nodejs.org/api/http.html#class-httpincomingmessage)
161
165
  or [http2.ClientHttp2Stream](https://nodejs.org/api/http2.html#class-clienthttp2stream) which is respectively
162
- readable and duplex streams
166
+ readable and duplex streams.
163
167
  * if `digest: true` & `parse: true`
164
- * `body` **{string | Array | Buffer | Object}** The body based on its content type
168
+ * `body` **{string | Array | Buffer | Object}** The body based on its content type.
165
169
  * if `digest: false`
166
- * `arrayBuffer` **{AsyncFunction}** Reads the response and returns **ArrayBuffer**
167
- * `blob` **{AsyncFunction}** Reads the response and returns **Blob**
168
- * `body` **{AsyncFunction}** Reads the response and returns **Buffer** if `parse: false`
169
- * `bytes` **{AsyncFunction}** Reads the response and returns **Uint8Array**
170
- * `json` **{AsyncFunction}** Reads the response and returns **Object**
171
- * `text` **{AsyncFunction}** Reads the response and returns **String**
172
- * `bodyUsed` **{boolean}** Indicates whether the response was read or not
173
- * `cookies` **{undefined | Cookies}** The cookies sent and received with the response
174
- * `headers` **{Object}** The headers received with the response
175
- * `httpVersion` **{string}** Indicates a protocol version negotiated with the server
176
- * `ok` **{boolean}** Indicates if the response was successful (statusCode: **200-299**)
177
- * `redirected` **{boolean}** Indicates if the response is the result of a redirect
178
- * `statusCode` **{number}** Indicates the status code of the response
179
- * `trailers` **{undefined | Object}** The trailer headers received with the response
170
+ * `arrayBuffer` **{AsyncFunction}** Reads the response and returns **ArrayBuffer**.
171
+ * `blob` **{AsyncFunction}** Reads the response and returns **Blob**.
172
+ * `body` **{AsyncFunction}** Reads the response and returns **Buffer** if `parse: false`.
173
+ * `bytes` **{AsyncFunction}** Reads the response and returns **Uint8Array**.
174
+ * `json` **{AsyncFunction}** Reads the response and returns **Object**.
175
+ * `text` **{AsyncFunction}** Reads the response and returns **String**.
176
+ * `bodyUsed` **{boolean}** Indicates whether the response was read or not.
177
+ * `cookies` **{undefined | Cookies}** The cookies sent and received with the response.
178
+ * `headers` **{Object}** The headers received with the response.
179
+ * `httpVersion` **{string}** Indicates a protocol version negotiated with the server.
180
+ * `ok` **{boolean}** Indicates if the response was successful (statusCode: **200-299**).
181
+ * `redirected` **{boolean}** Indicates if the response is the result of a redirect.
182
+ * `statusCode` **{number}** Indicates the status code of the response.
183
+ * `trailers` **{undefined | Object}** The trailer headers received with the response.
180
184
 
181
185
  ---
182
186
 
@@ -204,7 +208,7 @@ const rk = rekwest.extend({
204
208
  const params = {
205
209
  id: '[uid]',
206
210
  signature: '[code]',
207
- variant: 'A',
211
+ variant: '[any]',
208
212
  };
209
213
  const signal = AbortSignal.timeout(3e4);
210
214
  const url = '/somewhat/endpoint';
@@ -225,9 +229,9 @@ console.log(res.body);
225
229
 
226
230
  The method with limited functionality to use with streams and/or pipes.
227
231
 
228
- * No automata (redirects & retries)
229
- * Pass `h2: true` in options to use HTTP/2 protocol
230
- * Use `ackn({ url: URL })` method in advance to check the available protocols
232
+ * No automata (redirects & retries).
233
+ * Pass `h2: true` in options to use HTTP/2 protocol.
234
+ * Use `ackn({ url: URL })` method in advance to check the available protocols.
231
235
 
232
236
  ```javascript
233
237
  import fs from 'node:fs';
package/dist/ackn.cjs CHANGED
@@ -5,10 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.ackn = void 0;
7
7
  var _nodeTls = require("node:tls");
8
- const ackn = options => new Promise((resolve, reject) => {
9
- const {
10
- url
11
- } = options;
8
+ const ackn = (options = {}) => new Promise((resolve, reject) => {
9
+ const url = new URL(options.url);
12
10
  const socket = (0, _nodeTls.connect)({
13
11
  ...options,
14
12
  ALPNProtocols: ['h2', 'http/1.1'],
@@ -27,11 +25,11 @@ const ackn = options => new Promise((resolve, reject) => {
27
25
  createConnection() {
28
26
  return socket;
29
27
  },
30
- h2: /h2c?/i.test(alpnProtocol),
28
+ h2: /\bh2\b/i.test(alpnProtocol),
31
29
  protocol: url.protocol
32
30
  });
33
31
  });
34
- socket.on('error', reject);
35
- socket.on('timeout', reject);
32
+ socket.once('error', reject);
33
+ socket.once('timeout', reject);
36
34
  });
37
35
  exports.ackn = ackn;
package/dist/formdata.cjs CHANGED
@@ -3,8 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.FormData = void 0;
7
- var _nodeBuffer = require("node:buffer");
6
+ exports.parseFormData = exports.isFormData = exports.fdToAsyncIterable = exports.FormData = void 0;
8
7
  var _nodeCrypto = require("node:crypto");
9
8
  var _nodeHttp = _interopRequireDefault(require("node:http2"));
10
9
  var _mediatypes = require("./mediatypes.cjs");
@@ -16,40 +15,24 @@ const {
16
15
  HTTP2_HEADER_CONTENT_TYPE
17
16
  } = _nodeHttp.default.constants;
18
17
  class FormData {
19
- static actuate(fd) {
20
- const boundary = (0, _nodeCrypto.randomBytes)(24).toString('hex');
21
- const contentType = `${_mediatypes.MULTIPART_FORM_DATA}; boundary=${boundary}`;
22
- const prefix = `--${boundary}${CRLF}${HTTP2_HEADER_CONTENT_DISPOSITION}: form-data`;
23
- const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22');
24
- const redress = str => str.replace(/\r?\n|\r/g, CRLF);
25
- return {
26
- contentType,
27
- async *[Symbol.asyncIterator]() {
28
- const encoder = new TextEncoder();
29
- for (const [name, val] of fd) {
30
- if (val.constructor === String) {
31
- yield encoder.encode(`${prefix}; name="${escape(redress(name))}"${CRLF.repeat(2)}${redress(val)}${CRLF}`);
32
- } else {
33
- yield encoder.encode(`${prefix}; name="${escape(redress(name))}"${val.name ? `; filename="${escape(val.name)}"` : ''}${CRLF}${HTTP2_HEADER_CONTENT_TYPE}: ${val.type || _mediatypes.APPLICATION_OCTET_STREAM}${CRLF.repeat(2)}`);
34
- yield* (0, _utils.tap)(val);
35
- yield new Uint8Array([13, 10]);
36
- }
37
- }
38
- yield encoder.encode(`--${boundary}--`);
18
+ static #ensureArgs(args, expect, method) {
19
+ if (args.length < expect) {
20
+ throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': ${expect} arguments required, but only ${args.length} present`);
21
+ }
22
+ if (method === 'forEach') {
23
+ if (args[0]?.constructor !== Function) {
24
+ throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expect} is not of type 'Function'`);
39
25
  }
40
- };
41
- }
42
- static alike(val) {
43
- return FormData.name === val?.[Symbol.toStringTag];
26
+ }
44
27
  }
45
- static #enfoldEntry(name, value, filename) {
28
+ static #formEntry(name, value, filename) {
46
29
  name = String(name).toWellFormed();
47
30
  filename &&= String(filename).toWellFormed();
48
- if ((0, _utils.isFileLike)(value)) {
49
- filename ??= value.name || 'blob';
50
- value = new _nodeBuffer.File([value], filename, value);
51
- } else if (this.#ensureInstance(value)) {
52
- value.name = filename;
31
+ if ((0, _utils.isBlobLike)(value)) {
32
+ filename ??= String(value.name ?? 'blob').toWellFormed();
33
+ value = new File([value], filename, value);
34
+ } else if ((0, _utils.isPipeStream)(value) || (0, _utils.isReadableStream)(value)) {
35
+ value.name = filename ?? 'blob';
53
36
  } else {
54
37
  value = String(value).toWellFormed();
55
38
  }
@@ -58,9 +41,6 @@ class FormData {
58
41
  value
59
42
  };
60
43
  }
61
- static #ensureInstance(val) {
62
- return (0, _utils.isFileLike)(val) || Object(val) === val && Reflect.has(val, Symbol.asyncIterator);
63
- }
64
44
  #entries = [];
65
45
  get [Symbol.toStringTag]() {
66
46
  return this.constructor.name;
@@ -70,10 +50,10 @@ class FormData {
70
50
  if (Array.isArray(input)) {
71
51
  if (!input.every(it => Array.isArray(it))) {
72
52
  throw new TypeError(`Failed to construct '${this[Symbol.toStringTag]}': The provided value cannot be converted to a sequence`);
73
- } else if (!input.every(it => it.length === 2)) {
53
+ }
54
+ if (!input.every(it => it.length === 2)) {
74
55
  throw new TypeError(`Failed to construct '${this[Symbol.toStringTag]}': Sequence initializer must only contain pair elements`);
75
56
  }
76
- input = Array.from(input);
77
57
  } else if (!Reflect.has(input, Symbol.iterator)) {
78
58
  input = Object.entries(input);
79
59
  }
@@ -82,35 +62,20 @@ class FormData {
82
62
  }
83
63
  }
84
64
  }
85
- #ensureArgs(args, expected, method) {
86
- if (args.length < expected) {
87
- throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': ${expected} arguments required, but only ${args.length} present`);
88
- }
89
- if (['append', 'set'].includes(method)) {
90
- if (args.length === 3 && !this.constructor.#ensureInstance(args[1])) {
91
- throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expected} is not of type 'Blob'`);
92
- }
93
- }
94
- if (method === 'forEach') {
95
- if (args[0]?.constructor !== Function) {
96
- throw new TypeError(`Failed to execute '${method}' on '${this[Symbol.toStringTag]}': parameter ${expected} is not of type 'Function'`);
97
- }
98
- }
99
- }
100
65
  append(...args) {
101
66
  (0, _utils.brandCheck)(this, FormData);
102
- this.#ensureArgs(args, 2, 'append');
103
- this.#entries.push(this.constructor.#enfoldEntry(...args));
67
+ this.constructor.#ensureArgs(args, 2, this.append.name);
68
+ this.#entries.push(this.constructor.#formEntry(...args));
104
69
  }
105
70
  delete(...args) {
106
71
  (0, _utils.brandCheck)(this, FormData);
107
- this.#ensureArgs(args, 1, 'delete');
72
+ this.constructor.#ensureArgs(args, 1, this.delete.name);
108
73
  const name = String(args[0]).toWellFormed();
109
74
  this.#entries = this.#entries.filter(it => it.name !== name);
110
75
  }
111
76
  forEach(...args) {
112
77
  (0, _utils.brandCheck)(this, FormData);
113
- this.#ensureArgs(args, 1, 'forEach');
78
+ this.constructor.#ensureArgs(args, 1, this.forEach.name);
114
79
  const [callback, thisArg] = args;
115
80
  for (const entry of this) {
116
81
  Reflect.apply(callback, thisArg, [...entry.reverse(), this]);
@@ -118,26 +83,26 @@ class FormData {
118
83
  }
119
84
  get(...args) {
120
85
  (0, _utils.brandCheck)(this, FormData);
121
- this.#ensureArgs(args, 1, 'get');
86
+ this.constructor.#ensureArgs(args, 1, this.get.name);
122
87
  const name = String(args[0]).toWellFormed();
123
88
  return this.#entries.find(it => it.name === name)?.value ?? null;
124
89
  }
125
90
  getAll(...args) {
126
91
  (0, _utils.brandCheck)(this, FormData);
127
- this.#ensureArgs(args, 1, 'getAll');
92
+ this.constructor.#ensureArgs(args, 1, this.getAll.name);
128
93
  const name = String(args[0]).toWellFormed();
129
94
  return this.#entries.filter(it => it.name === name).map(it => it.value);
130
95
  }
131
96
  has(...args) {
132
97
  (0, _utils.brandCheck)(this, FormData);
133
- this.#ensureArgs(args, 1, 'has');
98
+ this.constructor.#ensureArgs(args, 1, this.has.name);
134
99
  const name = String(args[0]).toWellFormed();
135
100
  return !!this.#entries.find(it => it.name === name);
136
101
  }
137
102
  set(...args) {
138
103
  (0, _utils.brandCheck)(this, FormData);
139
- this.#ensureArgs(args, 2, 'set');
140
- const entry = this.constructor.#enfoldEntry(...args);
104
+ this.constructor.#ensureArgs(args, 2, this.set.name);
105
+ const entry = this.constructor.#formEntry(...args);
141
106
  const idx = this.#entries.findIndex(it => it.name === entry.name);
142
107
  if (idx !== -1) {
143
108
  this.#entries.splice(idx, 1, entry);
@@ -171,4 +136,37 @@ class FormData {
171
136
  return this.entries();
172
137
  }
173
138
  }
174
- exports.FormData = FormData;
139
+ exports.FormData = FormData;
140
+ const fdToAsyncIterable = fd => {
141
+ const boundary = (0, _nodeCrypto.randomBytes)(32).toString('hex');
142
+ const contentType = `${_mediatypes.MULTIPART_FORM_DATA}; boundary=${boundary}`;
143
+ const prefix = `--${boundary}${CRLF}${HTTP2_HEADER_CONTENT_DISPOSITION}: form-data`;
144
+ const escape = str => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22');
145
+ const normalize = str => str.replace(/\r?\n|\r/g, CRLF);
146
+ return {
147
+ contentType,
148
+ async *[Symbol.asyncIterator]() {
149
+ const encoder = new TextEncoder();
150
+ for (const [name, val] of fd) {
151
+ if (val.constructor === String) {
152
+ yield encoder.encode(`${prefix}; name="${escape(normalize(name))}"${CRLF.repeat(2)}${normalize(val)}${CRLF}`);
153
+ } else {
154
+ yield encoder.encode(`${prefix}; name="${escape(normalize(name))}"${val.name ? `; filename="${escape(val.name)}"` : ''}${CRLF}${HTTP2_HEADER_CONTENT_TYPE}: ${val.type || _mediatypes.APPLICATION_OCTET_STREAM}${CRLF.repeat(2)}`);
155
+ yield* (0, _utils.tap)(val);
156
+ yield new Uint8Array([13, 10]);
157
+ }
158
+ }
159
+ yield encoder.encode(`--${boundary}--${CRLF}`);
160
+ }
161
+ };
162
+ };
163
+ exports.fdToAsyncIterable = fdToAsyncIterable;
164
+ const isFormData = val => FormData.name === val?.[Symbol.toStringTag];
165
+ exports.isFormData = isFormData;
166
+ const parseFormData = str => {
167
+ const rex = /^-+[^\r\n]+\r?\ncontent-disposition:\s*form-data;\s*name="(?<name>[^"]+)"(?:;\s*filename="(?<filename>[^"]+)")?(?:\r?\n[^\r\n:]+:[^\r\n]*)*\r?\n\r?\n(?<content>.*?)(?=\r?\n-+[^\r\n]+)/gims;
168
+ return [...str.matchAll(rex)].map(({
169
+ groups
170
+ }) => structuredClone(groups));
171
+ };
172
+ exports.parseFormData = parseFormData;
package/dist/index.cjs CHANGED
@@ -5,22 +5,8 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  var _exportNames = {
7
7
  constants: true,
8
- mediatypes: true,
9
- Blob: true,
10
- File: true
8
+ mediatypes: true
11
9
  };
12
- Object.defineProperty(exports, "Blob", {
13
- enumerable: true,
14
- get: function () {
15
- return _nodeBuffer.Blob;
16
- }
17
- });
18
- Object.defineProperty(exports, "File", {
19
- enumerable: true,
20
- get: function () {
21
- return _nodeBuffer.File;
22
- }
23
- });
24
10
  Object.defineProperty(exports, "constants", {
25
11
  enumerable: true,
26
12
  get: function () {
@@ -74,7 +60,6 @@ Object.keys(_validation).forEach(function (key) {
74
60
  }
75
61
  });
76
62
  });
77
- var _nodeBuffer = require("node:buffer");
78
63
  var _ackn = require("./ackn.cjs");
79
64
  Object.keys(_ackn).forEach(function (key) {
80
65
  if (key === "default" || key === "__esModule") return;
package/dist/mixin.cjs CHANGED
@@ -40,7 +40,7 @@ const mixin = (res, {
40
40
  value: async function () {
41
41
  (0, _utils.brandCheck)(this, res?.constructor);
42
42
  const val = await this.arrayBuffer();
43
- return new _nodeBuffer.Blob([val]);
43
+ return new Blob([val]);
44
44
  }
45
45
  },
46
46
  bytes: {
package/dist/transfer.cjs CHANGED
@@ -53,11 +53,9 @@ const transfer = async options => {
53
53
  } = url.protocol === 'http:' ? _nodeHttp.default : _nodeHttps.default;
54
54
  req = request(url, options);
55
55
  }
56
- (0, _utils.snoop)(client, req, options);
57
- req.once('aborted', reject);
58
- req.once('error', reject);
59
- req.once('frameError', reject);
60
- req.once('goaway', reject);
56
+ (0, _utils.snoop)(client, req, options, {
57
+ reject
58
+ });
61
59
  req.once('response', res => (0, _postflight.postflight)(req, res, options, {
62
60
  reject,
63
61
  resolve
@@ -72,12 +70,15 @@ const transfer = async options => {
72
70
  return res;
73
71
  } catch (err) {
74
72
  if ((0, _utils.isLikelyH2cPrefaceError)(err)) {
73
+ const {
74
+ retry
75
+ } = options;
75
76
  options = (0, _utils.deepMerge)(options, {
76
77
  h2: true,
77
78
  retry: {
78
- attempts: 1,
79
- errorCodes: [err.code],
80
- interval: 0
79
+ attempts: ++retry.attempts,
80
+ errorCodes: [err.code, ...retry.errorCodes],
81
+ interval: 1
81
82
  }
82
83
  });
83
84
  }
@@ -28,7 +28,7 @@ const transform = async options => {
28
28
  }
29
29
  if (!Buffer.isBuffer(body)) {
30
30
  switch (true) {
31
- case (0, _utils.isFileLike)(body):
31
+ case (0, _utils.isBlobLike)(body):
32
32
  {
33
33
  headers = {
34
34
  [HTTP2_HEADER_CONTENT_LENGTH]: body.size,
@@ -37,9 +37,9 @@ const transform = async options => {
37
37
  body = body.stream();
38
38
  break;
39
39
  }
40
- case _formdata.FormData.alike(body):
40
+ case (0, _formdata.isFormData)(body):
41
41
  {
42
- body = _formdata.FormData.actuate(body);
42
+ body = (0, _formdata.fdToAsyncIterable)(body);
43
43
  headers = {
44
44
  [HTTP2_HEADER_CONTENT_TYPE]: body.contentType
45
45
  };
package/dist/utils.cjs CHANGED
@@ -3,10 +3,9 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.stripHeaders = exports.snoop = exports.sameOrigin = exports.normalizeHeaders = exports.normalize = exports.isReadableStream = exports.isPipeStream = exports.isLikelyH2cPrefaceError = exports.isFileLike = exports.dispatch = exports.deepMerge = exports.cloneWith = exports.brandCheck = exports.augment = exports.addSearchParams = void 0;
6
+ exports.stripHeaders = exports.snoop = exports.sameOrigin = exports.normalizeHeaders = exports.normalize = exports.isReadableStream = exports.isPipeStream = exports.isLikelyH2cPrefaceError = exports.isBlobLike = exports.dispatch = exports.deepMerge = exports.cloneWith = exports.brandCheck = exports.augment = exports.addSearchParams = void 0;
7
7
  exports.tap = tap;
8
8
  exports.unwind = exports.toCamelCase = void 0;
9
- var _nodeBuffer = require("node:buffer");
10
9
  var _nodeHttp = _interopRequireDefault(require("node:http2"));
11
10
  var _nodeStream = require("node:stream");
12
11
  var _config = _interopRequireWildcard(require("./config.cjs"));
@@ -31,9 +30,7 @@ const addSearchParams = (url, params = {}) => {
31
30
  };
32
31
  exports.addSearchParams = addSearchParams;
33
32
  const augment = (res, headers, options) => {
34
- const {
35
- h2
36
- } = options;
33
+ const h2 = /\bh2c?\b/i.test(res.session?.alpnProtocol);
37
34
  if (h2) {
38
35
  Reflect.defineProperty(res, 'headers', {
39
36
  enumerable: true,
@@ -92,27 +89,20 @@ const dispatch = (req, {
92
89
  body
93
90
  }) => {
94
91
  if ((0, _nodeStream.isReadable)(body)) {
92
+ body.once('error', err => req.session && req.emit('error', err) && req.destroy() || req.destroy(err));
95
93
  body.pipe(req);
96
94
  } else {
97
95
  req.end(body);
98
96
  }
99
97
  };
100
98
  exports.dispatch = dispatch;
101
- const isFileLike = val => {
102
- return [_nodeBuffer.Blob, _nodeBuffer.File].some(it => val instanceof it);
103
- };
104
- exports.isFileLike = isFileLike;
105
- const isLikelyH2cPrefaceError = err => {
106
- return err.code === 'HPE_INVALID_CONSTANT';
107
- };
99
+ const isBlobLike = val => val instanceof Blob;
100
+ exports.isBlobLike = isBlobLike;
101
+ const isLikelyH2cPrefaceError = err => err.code === 'HPE_INVALID_CONSTANT';
108
102
  exports.isLikelyH2cPrefaceError = isLikelyH2cPrefaceError;
109
- const isPipeStream = val => {
110
- return val instanceof _nodeStream.Readable;
111
- };
103
+ const isPipeStream = val => val instanceof _nodeStream.Readable;
112
104
  exports.isPipeStream = isPipeStream;
113
- const isReadableStream = val => {
114
- return val instanceof ReadableStream;
115
- };
105
+ const isReadableStream = val => val instanceof ReadableStream;
116
106
  exports.isReadableStream = isReadableStream;
117
107
  const normalize = (url, options = {}) => {
118
108
  if (!options.redirected) {
@@ -120,7 +110,7 @@ const normalize = (url, options = {}) => {
120
110
  }
121
111
  return Object.assign(options, {
122
112
  headers: normalizeHeaders(options.headers),
123
- method: options.method.toUpperCase(),
113
+ method: options.method?.toUpperCase(),
124
114
  url: addSearchParams(normalizeUrl(new URL(url, options.baseURL), options), options.params)
125
115
  });
126
116
  };
@@ -156,9 +146,17 @@ function normalizeUrl(url, {
156
146
  }
157
147
  const sameOrigin = (a, b) => a.origin === b.origin;
158
148
  exports.sameOrigin = sameOrigin;
159
- const snoop = (client, req, options) => {
149
+ const snoop = (client, req, options, {
150
+ reject
151
+ } = {
152
+ reject: () => void 0
153
+ }) => {
154
+ req.once('aborted', reject);
160
155
  req.once('close', () => client?.close());
161
156
  req.once('end', () => client?.close());
157
+ req.once('error', reject);
158
+ req.once('frameError', reject);
159
+ req.once('goaway', reject);
162
160
  req.once('timeout', () => req.destroy(new _errors.TimeoutError(`Timed out after ${options.timeout} ms`)));
163
161
  req.once('trailers', trailers => {
164
162
  Reflect.defineProperty(req, 'trailers', {
@@ -178,8 +176,6 @@ async function* tap(val) {
178
176
  yield* val;
179
177
  } else if (val.stream) {
180
178
  yield* val.stream();
181
- } else {
182
- yield await val.arrayBuffer();
183
179
  }
184
180
  }
185
181
  const toCamelCase = str => str?.toLowerCase().replace(/\p{Punctuation}.|\p{White_Space}./gu, val => val.replace(/\p{Punctuation}+|\p{White_Space}+/gu, '').toUpperCase());
package/package.json CHANGED
@@ -11,10 +11,11 @@
11
11
  "devDependencies": {
12
12
  "@babel/cli": "^7.28.6",
13
13
  "@babel/core": "^7.29.0",
14
- "@babel/preset-env": "^7.29.0",
15
- "c8": "^10.1.3",
16
- "eslint": "^10.0.0",
17
- "eslint-config-ultra-refined": "^4.0.1",
14
+ "@babel/preset-env": "^7.29.2",
15
+ "@eslint/markdown": "^8.0.0",
16
+ "c8": "^11.0.0",
17
+ "eslint": "^10.1.0",
18
+ "eslint-config-ultra-refined": "^4.1.5",
18
19
  "mocha": "^11.7.5"
19
20
  },
20
21
  "engines": {
@@ -60,16 +61,16 @@
60
61
  "url": "git+https://github.com/bricss/rekwest.git"
61
62
  },
62
63
  "scripts": {
63
- "build": "rm -rf dist && npx babel src --out-dir dist --out-file-extension .cjs",
64
+ "build": "rm -rf dist && npx babel src --out-dir dist --out-file-extension .cjs && sh misc.sh",
64
65
  "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",
65
66
  "cert:ken": "openssl x509 -in localhost.cert -noout -text",
66
67
  "lint": "eslint --concurrency=auto",
67
- "prepack": "npm run build && sh misc.sh && npm run lint",
68
+ "prepack": "npm run build && npm run lint",
68
69
  "pretest": "rm -rf coverage && npm run cert:gen",
69
70
  "test": "mocha",
70
71
  "test:bail": "mocha --bail",
71
72
  "test:cover": "c8 --include=src --reporter=lcov --reporter=text npm test"
72
73
  },
73
74
  "type": "module",
74
- "version": "7.2.4"
75
+ "version": "7.2.6"
75
76
  }
package/src/ackn.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { connect } from 'node:tls';
2
2
 
3
- export const ackn = (options) => new Promise((resolve, reject) => {
4
- const { url } = options;
3
+ export const ackn = (options = {}) => new Promise((resolve, reject) => {
4
+ const url = new URL(options.url);
5
5
  const socket = connect({
6
6
  ...options,
7
7
  ALPNProtocols: [
@@ -23,11 +23,11 @@ export const ackn = (options) => new Promise((resolve, reject) => {
23
23
  createConnection() {
24
24
  return socket;
25
25
  },
26
- h2: /h2c?/i.test(alpnProtocol),
26
+ h2: /\bh2\b/i.test(alpnProtocol),
27
27
  protocol: url.protocol,
28
28
  });
29
29
  });
30
30
 
31
- socket.on('error', reject);
32
- socket.on('timeout', reject);
31
+ socket.once('error', reject);
32
+ socket.once('timeout', reject);
33
33
  });
package/src/formdata.js CHANGED
@@ -1,4 +1,3 @@
1
- import { File } from 'node:buffer';
2
1
  import { randomBytes } from 'node:crypto';
3
2
  import http2 from 'node:http2';
4
3
  import {
@@ -7,7 +6,9 @@ import {
7
6
  } from './mediatypes.js';
8
7
  import {
9
8
  brandCheck,
10
- isFileLike,
9
+ isBlobLike,
10
+ isPipeStream,
11
+ isReadableStream,
11
12
  tap,
12
13
  } from './utils.js';
13
14
 
@@ -19,58 +20,31 @@ const {
19
20
 
20
21
  export class FormData {
21
22
 
22
- static actuate(fd) {
23
- const boundary = randomBytes(24).toString('hex');
24
- const contentType = `${ MULTIPART_FORM_DATA }; boundary=${ boundary }`;
25
- const prefix = `--${ boundary }${ CRLF }${ HTTP2_HEADER_CONTENT_DISPOSITION }: form-data`;
26
-
27
- const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22');
28
- const redress = (str) => str.replace(/\r?\n|\r/g, CRLF);
29
-
30
- return {
31
- contentType,
32
- async* [Symbol.asyncIterator]() {
33
- const encoder = new TextEncoder();
34
-
35
- for (const [name, val] of fd) {
36
- if (val.constructor === String) {
37
- yield encoder.encode(`${ prefix }; name="${
38
- escape(redress(name))
39
- }"${ CRLF.repeat(2) }${ redress(val) }${ CRLF }`);
40
- } else {
41
- yield encoder.encode(`${ prefix }; name="${
42
- escape(redress(name))
43
- }"${ val.name ? `; filename="${ escape(val.name) }"` : '' }${ CRLF }${
44
- HTTP2_HEADER_CONTENT_TYPE
45
- }: ${
46
- val.type || APPLICATION_OCTET_STREAM
47
- }${ CRLF.repeat(2) }`);
48
- yield* tap(val);
49
- yield new Uint8Array([
50
- 13,
51
- 10,
52
- ]);
53
- }
54
- }
55
-
56
- yield encoder.encode(`--${ boundary }--`);
57
- },
58
- };
59
- }
23
+ static #ensureArgs(args, expect, method) {
24
+ if (args.length < expect) {
25
+ throw new TypeError(`Failed to execute '${ method }' on '${
26
+ this[Symbol.toStringTag]
27
+ }': ${ expect } arguments required, but only ${ args.length } present`);
28
+ }
60
29
 
61
- static alike(val) {
62
- return FormData.name === val?.[Symbol.toStringTag];
30
+ if (method === 'forEach') {
31
+ if (args[0]?.constructor !== Function) {
32
+ throw new TypeError(`Failed to execute '${ method }' on '${
33
+ this[Symbol.toStringTag]
34
+ }': parameter ${ expect } is not of type 'Function'`);
35
+ }
36
+ }
63
37
  }
64
38
 
65
- static #enfoldEntry(name, value, filename) {
39
+ static #formEntry(name, value, filename) {
66
40
  name = String(name).toWellFormed();
67
41
  filename &&= String(filename).toWellFormed();
68
42
 
69
- if (isFileLike(value)) {
70
- filename ??= value.name || 'blob';
43
+ if (isBlobLike(value)) {
44
+ filename ??= String(value.name ?? 'blob').toWellFormed();
71
45
  value = new File([value], filename, value);
72
- } else if (this.#ensureInstance(value)) {
73
- value.name = filename;
46
+ } else if (isPipeStream(value) || isReadableStream(value)) {
47
+ value.name = filename ?? 'blob';
74
48
  } else {
75
49
  value = String(value).toWellFormed();
76
50
  }
@@ -81,10 +55,6 @@ export class FormData {
81
55
  };
82
56
  }
83
57
 
84
- static #ensureInstance(val) {
85
- return isFileLike(val) || (Object(val) === val && Reflect.has(val, Symbol.asyncIterator));
86
- }
87
-
88
58
  #entries = [];
89
59
 
90
60
  get [Symbol.toStringTag]() {
@@ -98,13 +68,13 @@ export class FormData {
98
68
  throw new TypeError(`Failed to construct '${
99
69
  this[Symbol.toStringTag]
100
70
  }': The provided value cannot be converted to a sequence`);
101
- } else if (!input.every((it) => it.length === 2)) {
71
+ }
72
+
73
+ if (!input.every((it) => it.length === 2)) {
102
74
  throw new TypeError(`Failed to construct '${
103
75
  this[Symbol.toStringTag]
104
76
  }': Sequence initializer must only contain pair elements`);
105
77
  }
106
-
107
- input = Array.from(input);
108
78
  } else if (!Reflect.has(input, Symbol.iterator)) {
109
79
  input = Object.entries(input);
110
80
  }
@@ -115,42 +85,15 @@ export class FormData {
115
85
  }
116
86
  }
117
87
 
118
- #ensureArgs(args, expected, method) {
119
- if (args.length < expected) {
120
- throw new TypeError(`Failed to execute '${ method }' on '${
121
- this[Symbol.toStringTag]
122
- }': ${ expected } arguments required, but only ${ args.length } present`);
123
- }
124
-
125
- if ([
126
- 'append',
127
- 'set',
128
- ].includes(method)) {
129
- if (args.length === 3 && !this.constructor.#ensureInstance(args[1])) {
130
- throw new TypeError(`Failed to execute '${ method }' on '${
131
- this[Symbol.toStringTag]
132
- }': parameter ${ expected } is not of type 'Blob'`);
133
- }
134
- }
135
-
136
- if (method === 'forEach') {
137
- if (args[0]?.constructor !== Function) {
138
- throw new TypeError(`Failed to execute '${ method }' on '${
139
- this[Symbol.toStringTag]
140
- }': parameter ${ expected } is not of type 'Function'`);
141
- }
142
- }
143
- }
144
-
145
88
  append(...args) {
146
89
  brandCheck(this, FormData);
147
- this.#ensureArgs(args, 2, 'append');
148
- this.#entries.push(this.constructor.#enfoldEntry(...args));
90
+ this.constructor.#ensureArgs(args, 2, this.append.name);
91
+ this.#entries.push(this.constructor.#formEntry(...args));
149
92
  }
150
93
 
151
94
  delete(...args) {
152
95
  brandCheck(this, FormData);
153
- this.#ensureArgs(args, 1, 'delete');
96
+ this.constructor.#ensureArgs(args, 1, this.delete.name);
154
97
  const name = String(args[0]).toWellFormed();
155
98
 
156
99
  this.#entries = this.#entries.filter((it) => it.name !== name);
@@ -158,7 +101,7 @@ export class FormData {
158
101
 
159
102
  forEach(...args) {
160
103
  brandCheck(this, FormData);
161
- this.#ensureArgs(args, 1, 'forEach');
104
+ this.constructor.#ensureArgs(args, 1, this.forEach.name);
162
105
  const [callback, thisArg] = args;
163
106
 
164
107
  for (const entry of this) {
@@ -171,7 +114,7 @@ export class FormData {
171
114
 
172
115
  get(...args) {
173
116
  brandCheck(this, FormData);
174
- this.#ensureArgs(args, 1, 'get');
117
+ this.constructor.#ensureArgs(args, 1, this.get.name);
175
118
  const name = String(args[0]).toWellFormed();
176
119
 
177
120
  return this.#entries.find((it) => it.name === name)?.value ?? null;
@@ -179,7 +122,7 @@ export class FormData {
179
122
 
180
123
  getAll(...args) {
181
124
  brandCheck(this, FormData);
182
- this.#ensureArgs(args, 1, 'getAll');
125
+ this.constructor.#ensureArgs(args, 1, this.getAll.name);
183
126
  const name = String(args[0]).toWellFormed();
184
127
 
185
128
  return this.#entries.filter((it) => it.name === name).map((it) => it.value);
@@ -187,7 +130,7 @@ export class FormData {
187
130
 
188
131
  has(...args) {
189
132
  brandCheck(this, FormData);
190
- this.#ensureArgs(args, 1, 'has');
133
+ this.constructor.#ensureArgs(args, 1, this.has.name);
191
134
  const name = String(args[0]).toWellFormed();
192
135
 
193
136
  return !!this.#entries.find((it) => it.name === name);
@@ -195,8 +138,8 @@ export class FormData {
195
138
 
196
139
  set(...args) {
197
140
  brandCheck(this, FormData);
198
- this.#ensureArgs(args, 2, 'set');
199
- const entry = this.constructor.#enfoldEntry(...args);
141
+ this.constructor.#ensureArgs(args, 2, this.set.name);
142
+ const entry = this.constructor.#formEntry(...args);
200
143
  const idx = this.#entries.findIndex((it) => it.name === entry.name);
201
144
 
202
145
  if (idx !== -1) {
@@ -237,3 +180,50 @@ export class FormData {
237
180
  }
238
181
 
239
182
  }
183
+
184
+ export const fdToAsyncIterable = (fd) => {
185
+ const boundary = randomBytes(32).toString('hex');
186
+ const contentType = `${ MULTIPART_FORM_DATA }; boundary=${ boundary }`;
187
+ const prefix = `--${ boundary }${ CRLF }${ HTTP2_HEADER_CONTENT_DISPOSITION }: form-data`;
188
+
189
+ const escape = (str) => str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22');
190
+ const normalize = (str) => str.replace(/\r?\n|\r/g, CRLF);
191
+
192
+ return {
193
+ contentType,
194
+ async* [Symbol.asyncIterator]() {
195
+ const encoder = new TextEncoder();
196
+
197
+ for (const [name, val] of fd) {
198
+ if (val.constructor === String) {
199
+ yield encoder.encode(`${ prefix }; name="${
200
+ escape(normalize(name))
201
+ }"${ CRLF.repeat(2) }${ normalize(val) }${ CRLF }`);
202
+ } else {
203
+ yield encoder.encode(`${ prefix }; name="${
204
+ escape(normalize(name))
205
+ }"${ val.name ? `; filename="${ escape(val.name) }"` : '' }${ CRLF }${
206
+ HTTP2_HEADER_CONTENT_TYPE
207
+ }: ${
208
+ val.type || APPLICATION_OCTET_STREAM
209
+ }${ CRLF.repeat(2) }`);
210
+ yield* tap(val);
211
+ yield new Uint8Array([
212
+ 13,
213
+ 10,
214
+ ]);
215
+ }
216
+ }
217
+
218
+ yield encoder.encode(`--${ boundary }--${ CRLF }`);
219
+ },
220
+ };
221
+ };
222
+
223
+ export const isFormData = (val) => FormData.name === val?.[Symbol.toStringTag];
224
+
225
+ export const parseFormData = (str) => {
226
+ const rex = /^-+[^\r\n]+\r?\ncontent-disposition:\s*form-data;\s*name="(?<name>[^"]+)"(?:;\s*filename="(?<filename>[^"]+)")?(?:\r?\n[^\r\n:]+:[^\r\n]*)*\r?\n\r?\n(?<content>.*?)(?=\r?\n-+[^\r\n]+)/gims;
227
+
228
+ return [...str.matchAll(rex)].map(({ groups }) => structuredClone(groups));
229
+ };
package/src/index.js CHANGED
@@ -14,10 +14,6 @@ import {
14
14
  } from './utils.js';
15
15
  import { validation } from './validation.js';
16
16
 
17
- export {
18
- Blob,
19
- File,
20
- } from 'node:buffer';
21
17
  export { constants } from 'node:http2';
22
18
  export * from './ackn.js';
23
19
  export * from './codecs.js';
package/src/mixin.js CHANGED
@@ -1,7 +1,4 @@
1
- import {
2
- Blob,
3
- isUtf8,
4
- } from 'node:buffer';
1
+ import { isUtf8 } from 'node:buffer';
5
2
  import http2 from 'node:http2';
6
3
  import { buffer } from 'node:stream/consumers';
7
4
  import { MIMEType } from 'node:util';
package/src/transfer.js CHANGED
@@ -53,12 +53,8 @@ export const transfer = async (options) => {
53
53
  req = request(url, options);
54
54
  }
55
55
 
56
- snoop(client, req, options);
56
+ snoop(client, req, options, { reject });
57
57
 
58
- req.once('aborted', reject);
59
- req.once('error', reject);
60
- req.once('frameError', reject);
61
- req.once('goaway', reject);
62
58
  req.once('response', (res) => postflight(req, res, options, {
63
59
  reject, resolve,
64
60
  }));
@@ -76,12 +72,17 @@ export const transfer = async (options) => {
76
72
  return res;
77
73
  } catch (err) {
78
74
  if (isLikelyH2cPrefaceError(err)) {
75
+ const { retry } = options;
76
+
79
77
  options = deepMerge(options, {
80
78
  h2: true,
81
79
  retry: {
82
- attempts: 1,
83
- errorCodes: [err.code],
84
- interval: 0,
80
+ attempts: ++retry.attempts,
81
+ errorCodes: [
82
+ err.code,
83
+ ...retry.errorCodes,
84
+ ],
85
+ interval: 1,
85
86
  },
86
87
  });
87
88
  }
package/src/transform.js CHANGED
@@ -6,14 +6,17 @@ import {
6
6
  import { buffer } from 'node:stream/consumers';
7
7
  import { types } from 'node:util';
8
8
  import { encode } from './codecs.js';
9
- import { FormData } from './formdata.js';
9
+ import {
10
+ fdToAsyncIterable,
11
+ isFormData,
12
+ } from './formdata.js';
10
13
  import {
11
14
  APPLICATION_FORM_URLENCODED,
12
15
  APPLICATION_JSON,
13
16
  APPLICATION_OCTET_STREAM,
14
17
  } from './mediatypes.js';
15
18
  import {
16
- isFileLike,
19
+ isBlobLike,
17
20
  isReadableStream,
18
21
  } from './utils.js';
19
22
 
@@ -32,7 +35,7 @@ export const transform = async (options) => {
32
35
 
33
36
  if (!Buffer.isBuffer(body)) {
34
37
  switch (true) {
35
- case isFileLike(body): {
38
+ case isBlobLike(body): {
36
39
  headers = {
37
40
  [HTTP2_HEADER_CONTENT_LENGTH]: body.size,
38
41
  [HTTP2_HEADER_CONTENT_TYPE]: body.type || APPLICATION_OCTET_STREAM,
@@ -41,8 +44,8 @@ export const transform = async (options) => {
41
44
  break;
42
45
  }
43
46
 
44
- case FormData.alike(body): {
45
- body = FormData.actuate(body);
47
+ case isFormData(body): {
48
+ body = fdToAsyncIterable(body);
46
49
  headers = { [HTTP2_HEADER_CONTENT_TYPE]: body.contentType };
47
50
  break;
48
51
  }
package/src/utils.js CHANGED
@@ -1,7 +1,3 @@
1
- import {
2
- Blob,
3
- File,
4
- } from 'node:buffer';
5
1
  import http2 from 'node:http2';
6
2
  import {
7
3
  isReadable,
@@ -30,7 +26,7 @@ export const addSearchParams = (url, params = {}) => {
30
26
  };
31
27
 
32
28
  export const augment = (res, headers, options) => {
33
- const { h2 } = options;
29
+ const h2 = /\bh2c?\b/i.test(res.session?.alpnProtocol);
34
30
 
35
31
  if (h2) {
36
32
  Reflect.defineProperty(res, 'headers', {
@@ -96,30 +92,20 @@ export const deepMerge = (target, ...rest) => {
96
92
 
97
93
  export const dispatch = (req, { body }) => {
98
94
  if (isReadable(body)) {
95
+ body.once('error', (err) => (req.session && req.emit('error', err) && req.destroy()) || req.destroy(err));
99
96
  body.pipe(req);
100
97
  } else {
101
98
  req.end(body);
102
99
  }
103
100
  };
104
101
 
105
- export const isFileLike = (val) => {
106
- return [
107
- Blob,
108
- File,
109
- ].some((it) => val instanceof it);
110
- };
102
+ export const isBlobLike = (val) => val instanceof Blob;
111
103
 
112
- export const isLikelyH2cPrefaceError = (err) => {
113
- return err.code === 'HPE_INVALID_CONSTANT';
114
- };
104
+ export const isLikelyH2cPrefaceError = (err) => err.code === 'HPE_INVALID_CONSTANT';
115
105
 
116
- export const isPipeStream = (val) => {
117
- return val instanceof Readable;
118
- };
106
+ export const isPipeStream = (val) => val instanceof Readable;
119
107
 
120
- export const isReadableStream = (val) => {
121
- return val instanceof ReadableStream;
122
- };
108
+ export const isReadableStream = (val) => val instanceof ReadableStream;
123
109
 
124
110
  export const normalize = (url, options = {}) => {
125
111
  if (!options.redirected) {
@@ -128,7 +114,7 @@ export const normalize = (url, options = {}) => {
128
114
 
129
115
  return Object.assign(options, {
130
116
  headers: normalizeHeaders(options.headers),
131
- method: options.method.toUpperCase(),
117
+ method: options.method?.toUpperCase(),
132
118
  url: addSearchParams(normalizeUrl(new URL(url, options.baseURL), options), options.params),
133
119
  });
134
120
  };
@@ -169,9 +155,13 @@ function normalizeUrl(url, { trimTrailingSlashes, stripTrailingSlash } = {}) {
169
155
 
170
156
  export const sameOrigin = (a, b) => a.origin === b.origin;
171
157
 
172
- export const snoop = (client, req, options) => {
158
+ export const snoop = (client, req, options, { reject } = { reject: () => void 0 }) => {
159
+ req.once('aborted', reject);
173
160
  req.once('close', () => client?.close());
174
161
  req.once('end', () => client?.close());
162
+ req.once('error', reject);
163
+ req.once('frameError', reject);
164
+ req.once('goaway', reject);
175
165
  req.once('timeout', () => req.destroy(new TimeoutError(`Timed out after ${ options.timeout } ms`)));
176
166
  req.once('trailers', (trailers) => {
177
167
  Reflect.defineProperty(req, 'trailers', {
@@ -192,8 +182,6 @@ export async function* tap(val) {
192
182
  yield* val;
193
183
  } else if (val.stream) {
194
184
  yield* val.stream();
195
- } else {
196
- yield await val.arrayBuffer();
197
185
  }
198
186
  }
199
187