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 +62 -58
- package/dist/ackn.cjs +5 -7
- package/dist/formdata.cjs +60 -62
- package/dist/index.cjs +1 -16
- package/dist/mixin.cjs +1 -1
- package/dist/transfer.cjs +9 -8
- package/dist/transform.cjs +3 -3
- package/dist/utils.cjs +18 -22
- package/package.json +8 -7
- package/src/ackn.js +5 -5
- package/src/formdata.js +80 -90
- package/src/index.js +0 -4
- package/src/mixin.js +1 -4
- package/src/transfer.js +9 -8
- package/src/transform.js +8 -5
- package/src/utils.js +12 -24
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 `>=
|
|
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',
|
|
52
|
-
/**
|
|
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
|
|
84
|
+
const rbl = Readable.from('bits');
|
|
85
|
+
const rds = ReadableStream.from('bits');
|
|
86
86
|
|
|
87
87
|
const fd = new FormData({
|
|
88
|
-
aux: Date
|
|
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('
|
|
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',
|
|
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: '
|
|
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: /
|
|
28
|
+
h2: /\bh2\b/i.test(alpnProtocol),
|
|
31
29
|
protocol: url.protocol
|
|
32
30
|
});
|
|
33
31
|
});
|
|
34
|
-
socket.
|
|
35
|
-
socket.
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 #
|
|
28
|
+
static #formEntry(name, value, filename) {
|
|
46
29
|
name = String(name).toWellFormed();
|
|
47
30
|
filename &&= String(filename).toWellFormed();
|
|
48
|
-
if ((0, _utils.
|
|
49
|
-
filename ??= value.name
|
|
50
|
-
value = new
|
|
51
|
-
} else if (
|
|
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
|
-
}
|
|
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,
|
|
103
|
-
this.#entries.push(this.constructor.#
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
140
|
-
const entry = this.constructor.#
|
|
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
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
|
-
|
|
58
|
-
|
|
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:
|
|
79
|
-
errorCodes: [err.code],
|
|
80
|
-
interval:
|
|
79
|
+
attempts: ++retry.attempts,
|
|
80
|
+
errorCodes: [err.code, ...retry.errorCodes],
|
|
81
|
+
interval: 1
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
}
|
package/dist/transform.cjs
CHANGED
|
@@ -28,7 +28,7 @@ const transform = async options => {
|
|
|
28
28
|
}
|
|
29
29
|
if (!Buffer.isBuffer(body)) {
|
|
30
30
|
switch (true) {
|
|
31
|
-
case (0, _utils.
|
|
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.
|
|
40
|
+
case (0, _formdata.isFormData)(body):
|
|
41
41
|
{
|
|
42
|
-
body = _formdata.
|
|
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.
|
|
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
|
|
102
|
-
|
|
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
|
|
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.
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"eslint
|
|
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 &&
|
|
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.
|
|
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
|
|
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: /
|
|
26
|
+
h2: /\bh2\b/i.test(alpnProtocol),
|
|
27
27
|
protocol: url.protocol,
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
socket.
|
|
32
|
-
socket.
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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 #
|
|
39
|
+
static #formEntry(name, value, filename) {
|
|
66
40
|
name = String(name).toWellFormed();
|
|
67
41
|
filename &&= String(filename).toWellFormed();
|
|
68
42
|
|
|
69
|
-
if (
|
|
70
|
-
filename ??= value.name
|
|
43
|
+
if (isBlobLike(value)) {
|
|
44
|
+
filename ??= String(value.name ?? 'blob').toWellFormed();
|
|
71
45
|
value = new File([value], filename, value);
|
|
72
|
-
} else if (
|
|
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
|
-
}
|
|
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,
|
|
148
|
-
this.#entries.push(this.constructor.#
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
199
|
-
const entry = this.constructor.#
|
|
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
package/src/mixin.js
CHANGED
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:
|
|
83
|
-
errorCodes: [
|
|
84
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
45
|
-
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
|
|
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
|
|
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
|
|
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
|
|