rekwest 7.2.4 → 7.2.5

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