@repost/client 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -85
- package/dist/cjs/index-e0zw7VDH.cjs +2987 -0
- package/dist/cjs/index-e0zw7VDH.cjs.map +1 -0
- package/dist/cjs/index.cjs +23 -1046
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/node-Dd92VNS7.cjs +841 -0
- package/dist/cjs/node-Dd92VNS7.cjs.map +1 -0
- package/dist/cjs/observer-dykSFHYi.cjs +69 -0
- package/dist/cjs/observer-dykSFHYi.cjs.map +1 -0
- package/dist/cjs/otel/index.cjs +99 -0
- package/dist/cjs/otel/index.cjs.map +1 -0
- package/dist/cjs/testing/index.cjs +257 -0
- package/dist/cjs/testing/index.cjs.map +1 -0
- package/dist/cjs/types/config.d.ts +28 -0
- package/dist/cjs/types/errors.d.ts +83 -0
- package/dist/cjs/types/index.d.ts +8 -3
- package/dist/cjs/types/observer.d.ts +44 -0
- package/dist/cjs/types/operation-events.d.ts +8 -0
- package/dist/cjs/types/operation-input.d.ts +5 -0
- package/dist/cjs/types/operation-policy.d.ts +23 -0
- package/dist/cjs/types/operation.d.ts +14 -0
- package/dist/cjs/types/otel/index.d.ts +2 -0
- package/dist/cjs/types/otel/metrics.d.ts +5 -0
- package/dist/cjs/types/otel/telemetry.d.ts +5 -0
- package/dist/cjs/types/runtime-state.d.ts +45 -0
- package/dist/cjs/types/serialize.d.ts +9 -3
- package/dist/cjs/types/telemetry.d.ts +43 -0
- package/dist/cjs/types/testing/deterministic.d.ts +19 -0
- package/dist/cjs/types/testing/index.d.ts +16 -0
- package/dist/cjs/types/testing/manual-clock.d.ts +12 -0
- package/dist/cjs/types/testing/recording-observer.d.ts +11 -0
- package/dist/cjs/types/testing/stub-transport.d.ts +35 -0
- package/dist/cjs/types/transport/connect-proxy.d.ts +30 -0
- package/dist/cjs/types/transport/failure.d.ts +7 -0
- package/dist/cjs/types/transport/framing.d.ts +8 -0
- package/dist/cjs/types/transport/h2.d.ts +3 -0
- package/dist/cjs/types/transport/node.d.ts +3 -0
- package/dist/cjs/types/transport/response-reader.d.ts +6 -0
- package/dist/cjs/types/transport/strict-json.d.ts +5 -0
- package/dist/cjs/types/transport/tls.d.ts +14 -0
- package/dist/cjs/types/transport/undici-compat.d.ts +47 -0
- package/dist/cjs/types/transport.d.ts +42 -19
- package/dist/cjs/types/types.d.ts +146 -23
- package/dist/cjs/types/version.d.ts +2 -0
- package/dist/cjs/types/webhooks.d.ts +15 -0
- package/dist/cjs/version-CGqSWJir.cjs +7 -0
- package/dist/cjs/version-CGqSWJir.cjs.map +1 -0
- package/dist/esm/index-DRD5FBax.mjs +2967 -0
- package/dist/esm/index-DRD5FBax.mjs.map +1 -0
- package/dist/esm/index.mjs +3 -1041
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/node-BAJt_qhB.mjs +839 -0
- package/dist/esm/node-BAJt_qhB.mjs.map +1 -0
- package/dist/esm/observer-Dns2X4wE.mjs +66 -0
- package/dist/esm/observer-Dns2X4wE.mjs.map +1 -0
- package/dist/esm/otel/index.mjs +96 -0
- package/dist/esm/otel/index.mjs.map +1 -0
- package/dist/esm/testing/index.mjs +248 -0
- package/dist/esm/testing/index.mjs.map +1 -0
- package/dist/esm/version-Dp5vd2nu.mjs +5 -0
- package/dist/esm/version-Dp5vd2nu.mjs.map +1 -0
- package/index.d.ts +1 -1
- package/index.js +3 -4
- package/index.mjs +1 -1
- package/otel/index.d.ts +1 -0
- package/otel/index.js +3 -0
- package/otel/index.mjs +1 -0
- package/package.json +38 -9
- package/runtime/index.d.ts +1 -1
- package/runtime/index.js +2 -2
- package/runtime/index.mjs +1 -1
- package/scripts/postinstall.js +2 -2
- package/testing/index.d.ts +1 -0
- package/testing/index.js +3 -0
- package/testing/index.mjs +1 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
import { e as errorCode$1, c as classifyNetworkFailure } from './index-DRD5FBax.mjs';
|
|
2
|
+
import { gunzip } from 'node:zlib';
|
|
3
|
+
import { buildConnector, Pool } from 'undici';
|
|
4
|
+
import { connect, isIP } from 'node:net';
|
|
5
|
+
import { connect as connect$1 } from 'node:tls';
|
|
6
|
+
import { connect as connect$2, constants } from 'node:http2';
|
|
7
|
+
import './version-Dp5vd2nu.mjs';
|
|
8
|
+
import './observer-Dns2X4wE.mjs';
|
|
9
|
+
|
|
10
|
+
/** Builds the exact ordered fields used by the Node H1/H2 dispatcher. */
|
|
11
|
+
function frameRequest(request) {
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const authority = explicitAuthority(request.url);
|
|
14
|
+
const headers = [
|
|
15
|
+
"host", authority,
|
|
16
|
+
"content-length", String(request.bodyBytes.byteLength),
|
|
17
|
+
];
|
|
18
|
+
for (const [name, value] of request.headers)
|
|
19
|
+
headers.push(name, value);
|
|
20
|
+
return {
|
|
21
|
+
origin: `${url.protocol}//${authority}`,
|
|
22
|
+
path: `${url.pathname}${url.search}`,
|
|
23
|
+
headers,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function explicitAuthority(value) {
|
|
27
|
+
const start = value.indexOf("://");
|
|
28
|
+
const authorityStart = start < 0 ? 0 : start + 3;
|
|
29
|
+
const tail = value.slice(authorityStart);
|
|
30
|
+
const authorityEnd = tail.search(/[/?#]/u);
|
|
31
|
+
return tail.slice(0, authorityEnd < 0 ? undefined : authorityEnd);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const COMPRESSED_LIMIT = 1048576;
|
|
35
|
+
const DECOMPRESSED_LIMIT = 1048576;
|
|
36
|
+
const ERROR_INSPECTION_LIMIT = 65536;
|
|
37
|
+
const EXPANSION_RATIO_LIMIT = 100;
|
|
38
|
+
const HEADER_FIELD_LIMIT = 100;
|
|
39
|
+
const HEADER_BYTES_LIMIT = 65536;
|
|
40
|
+
const HEADER_NAME_LIMIT = 256;
|
|
41
|
+
const HEADER_VALUE_LIMIT = 8192;
|
|
42
|
+
const encoder = new TextEncoder();
|
|
43
|
+
const asciiOnlyHeaders = new Set(["content-encoding", "content-length", "content-type", "retry-after", "location"]);
|
|
44
|
+
const framingHeaders = new Set(["connection", "date", "keep-alive", "transfer-encoding"]);
|
|
45
|
+
async function readResponse(status, rawHeaders, chunks, compressedBytes, transportTruncated, compressedLimit = COMPRESSED_LIMIT) {
|
|
46
|
+
const headerSnapshot = snapshotHeaders(rawHeaders);
|
|
47
|
+
const common = {
|
|
48
|
+
kind: "response",
|
|
49
|
+
status,
|
|
50
|
+
headers: headerSnapshot.headers,
|
|
51
|
+
compressedBytes,
|
|
52
|
+
responseHeaderFields: headerSnapshot.fields,
|
|
53
|
+
responseHeaderBytes: headerSnapshot.bytes,
|
|
54
|
+
};
|
|
55
|
+
const success = status >= 200 && status <= 299;
|
|
56
|
+
if (headerSnapshot.protocolInvalid || headerSnapshot.limitExceeded) {
|
|
57
|
+
return {
|
|
58
|
+
...common,
|
|
59
|
+
bodyBytes: new Uint8Array(),
|
|
60
|
+
compressedBytes: 0,
|
|
61
|
+
decompressedBytes: 0,
|
|
62
|
+
truncated: transportTruncated,
|
|
63
|
+
headerLimitExceeded: true,
|
|
64
|
+
...(headerSnapshot.protocolInvalid ? { protocolError: true } : {}),
|
|
65
|
+
...(headerSnapshot.limitExceeded ? { limitExceeded: success, nonRetryable: !success } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const outputLimit = success ? DECOMPRESSED_LIMIT : ERROR_INSPECTION_LIMIT;
|
|
69
|
+
const encoding = contentEncoding(headerSnapshot.headers);
|
|
70
|
+
if (encoding === null) {
|
|
71
|
+
return { ...common, bodyBytes: new Uint8Array(), compressedBytes: 0, decompressedBytes: 0, truncated: false, protocolError: true };
|
|
72
|
+
}
|
|
73
|
+
const declaredLength = singleHeader(headerSnapshot.headers, "content-length");
|
|
74
|
+
if (success && declaredLength !== null && (!/^(?:0|[1-9][0-9]*)$/u.test(declaredLength) || Number(declaredLength) > compressedLimit)) {
|
|
75
|
+
return { ...common, bodyBytes: new Uint8Array(), decompressedBytes: 0, truncated: transportTruncated, limitExceeded: true };
|
|
76
|
+
}
|
|
77
|
+
if (transportTruncated || compressedBytes > compressedLimit) {
|
|
78
|
+
if (encoding === "identity") {
|
|
79
|
+
const overLimit = compressedBytes > compressedLimit;
|
|
80
|
+
return {
|
|
81
|
+
...common,
|
|
82
|
+
bodyBytes: concatenate(chunks, Math.min(outputLimit, compressedLimit)),
|
|
83
|
+
decompressedBytes: compressedBytes,
|
|
84
|
+
truncated: overLimit ? false : transportTruncated,
|
|
85
|
+
...(success && overLimit ? { limitExceeded: true } : {}),
|
|
86
|
+
...(success && transportTruncated && !overLimit ? { protocolError: true } : {}),
|
|
87
|
+
...(!success && overLimit ? { nonRetryable: true } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
...common,
|
|
92
|
+
bodyBytes: new Uint8Array(),
|
|
93
|
+
decompressedBytes: 0,
|
|
94
|
+
truncated: transportTruncated,
|
|
95
|
+
protocolError: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const compressed = concatenate(chunks, compressedLimit);
|
|
99
|
+
if (encoding === "identity") {
|
|
100
|
+
const truncated = compressed.length > outputLimit;
|
|
101
|
+
const bodyBytes = truncated ? compressed.slice(0, outputLimit) : compressed;
|
|
102
|
+
return { ...common, bodyBytes, decompressedBytes: compressed.length, truncated, limitExceeded: success && truncated };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const decoded = new Uint8Array(await new Promise((resolve, reject) => {
|
|
106
|
+
gunzip(compressed, { maxOutputLength: outputLimit + 1 }, (error, value) => {
|
|
107
|
+
if (error)
|
|
108
|
+
reject(error);
|
|
109
|
+
else
|
|
110
|
+
resolve(value);
|
|
111
|
+
});
|
|
112
|
+
}));
|
|
113
|
+
const ratioExceeded = compressed.length === 0 ? decoded.length > 0 : decoded.length > compressed.length * EXPANSION_RATIO_LIMIT;
|
|
114
|
+
const truncated = decoded.length > outputLimit;
|
|
115
|
+
const bodyBytes = decoded.slice(0, Math.min(decoded.length, outputLimit));
|
|
116
|
+
return {
|
|
117
|
+
...common,
|
|
118
|
+
bodyBytes,
|
|
119
|
+
decompressedBytes: decoded.length,
|
|
120
|
+
truncated,
|
|
121
|
+
...(ratioExceeded ? { protocolError: true } : { limitExceeded: success && truncated }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const limit = errorCode(error) === "ERR_BUFFER_TOO_LARGE";
|
|
126
|
+
return {
|
|
127
|
+
...common,
|
|
128
|
+
bodyBytes: new Uint8Array(),
|
|
129
|
+
decompressedBytes: limit ? outputLimit + 1 : 0,
|
|
130
|
+
truncated: limit,
|
|
131
|
+
...(limit ? { limitExceeded: success } : { protocolError: true }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function snapshotHeaders(raw) {
|
|
136
|
+
const headers = {};
|
|
137
|
+
let fields = 0;
|
|
138
|
+
let bytes = 0;
|
|
139
|
+
let protocolInvalid = raw.length % 2 !== 0;
|
|
140
|
+
let limitExceeded = false;
|
|
141
|
+
for (let index = 0; index + 1 < raw.length; index += 2) {
|
|
142
|
+
const name = Buffer.from(raw[index]).toString("latin1").toLowerCase();
|
|
143
|
+
const value = Buffer.from(raw[index + 1]).toString("latin1");
|
|
144
|
+
if (framingHeaders.has(name))
|
|
145
|
+
continue;
|
|
146
|
+
fields += 1;
|
|
147
|
+
const nameBytes = encoder.encode(name).length;
|
|
148
|
+
const valueBytes = encoder.encode(value).length;
|
|
149
|
+
bytes = saturatingAdd(bytes, nameBytes + 2 + valueBytes + 2);
|
|
150
|
+
if (fields > HEADER_FIELD_LIMIT || bytes > HEADER_BYTES_LIMIT || nameBytes > HEADER_NAME_LIMIT || valueBytes > HEADER_VALUE_LIMIT)
|
|
151
|
+
limitExceeded = true;
|
|
152
|
+
if (nameBytes < 1
|
|
153
|
+
|| !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u.test(name)
|
|
154
|
+
|| /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value)
|
|
155
|
+
|| (asciiOnlyHeaders.has(name) && /[^\u0000-\u007f]/u.test(value)))
|
|
156
|
+
protocolInvalid = true;
|
|
157
|
+
if (protocolInvalid || limitExceeded)
|
|
158
|
+
continue;
|
|
159
|
+
(headers[name] ?? (headers[name] = [])).push(value);
|
|
160
|
+
}
|
|
161
|
+
return { headers, fields, bytes, protocolInvalid, limitExceeded };
|
|
162
|
+
}
|
|
163
|
+
function contentEncoding(headers) {
|
|
164
|
+
const values = headers["content-encoding"] ?? [];
|
|
165
|
+
if (values.length === 0)
|
|
166
|
+
return "identity";
|
|
167
|
+
if (values.length !== 1)
|
|
168
|
+
return null;
|
|
169
|
+
const value = values[0]?.trim().toLowerCase();
|
|
170
|
+
return value === "identity" || value === "gzip" ? value : null;
|
|
171
|
+
}
|
|
172
|
+
function singleHeader(headers, name) {
|
|
173
|
+
const values = headers[name] ?? [];
|
|
174
|
+
return values.length === 1 ? values[0] ?? null : null;
|
|
175
|
+
}
|
|
176
|
+
function concatenate(chunks, limit) {
|
|
177
|
+
const length = Math.min(limit + 1, chunks.reduce((total, chunk) => saturatingAdd(total, chunk.length), 0));
|
|
178
|
+
const bytes = new Uint8Array(length);
|
|
179
|
+
let offset = 0;
|
|
180
|
+
for (const chunk of chunks) {
|
|
181
|
+
const part = chunk.subarray(0, length - offset);
|
|
182
|
+
bytes.set(part, offset);
|
|
183
|
+
offset += part.length;
|
|
184
|
+
if (offset === length)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
return bytes;
|
|
188
|
+
}
|
|
189
|
+
function saturatingAdd(left, right) {
|
|
190
|
+
return left > Number.MAX_SAFE_INTEGER - right ? Number.MAX_SAFE_INTEGER : left + right;
|
|
191
|
+
}
|
|
192
|
+
function errorCode(error) {
|
|
193
|
+
return error && typeof error === "object" && typeof error.code === "string"
|
|
194
|
+
? error.code
|
|
195
|
+
: "";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const HEADER_LIMIT = 65536;
|
|
199
|
+
async function connectThroughProxy(proxy, target, tls, allowH2, timeoutMs) {
|
|
200
|
+
const authority = `${target.hostname}:${target.port}`;
|
|
201
|
+
let authorization;
|
|
202
|
+
if (proxy.credentialsProvider) {
|
|
203
|
+
let credentials;
|
|
204
|
+
try {
|
|
205
|
+
credentials = await proxy.credentialsProvider();
|
|
206
|
+
}
|
|
207
|
+
catch (cause) {
|
|
208
|
+
throw proxyError("REPOST_PROXY_CREDENTIAL_PROVIDER", cause);
|
|
209
|
+
}
|
|
210
|
+
authorization = `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`, "utf8").toString("base64")}`;
|
|
211
|
+
}
|
|
212
|
+
const tunnel = await connectTunnel(proxy.url, authority, authorization, timeoutMs);
|
|
213
|
+
if (tunnel.status === 407) {
|
|
214
|
+
tunnel.socket.destroy();
|
|
215
|
+
throw proxyError("REPOST_PROXY_AUTH_REQUIRED");
|
|
216
|
+
}
|
|
217
|
+
if (tunnel.status !== 200) {
|
|
218
|
+
tunnel.socket.destroy();
|
|
219
|
+
throw proxyError("REPOST_PROXY_CONNECT_FAILED");
|
|
220
|
+
}
|
|
221
|
+
if (target.protocol !== "https:")
|
|
222
|
+
return tunnel.socket;
|
|
223
|
+
return secureTunnel(tunnel.socket, target, tls, allowH2, timeoutMs);
|
|
224
|
+
}
|
|
225
|
+
async function connectTunnel(proxyValue, authority, authorization, timeoutMs) {
|
|
226
|
+
const proxy = new URL(proxyValue);
|
|
227
|
+
if (proxy.protocol !== "http:" || proxy.username || proxy.password || proxy.pathname !== "/") {
|
|
228
|
+
throw proxyError("REPOST_PROXY_CONNECT_FAILED");
|
|
229
|
+
}
|
|
230
|
+
const socket = connect({ host: proxy.hostname, port: Number(proxy.port || 80) });
|
|
231
|
+
await socketReady(socket, timeoutMs);
|
|
232
|
+
socket.write(`CONNECT ${authority} HTTP/1.1\r\n`
|
|
233
|
+
+ `host: ${authority}\r\n`
|
|
234
|
+
+ (authorization ? `proxy-authorization: ${authorization}\r\n` : "")
|
|
235
|
+
+ "\r\n", "latin1");
|
|
236
|
+
let response = Buffer.alloc(0);
|
|
237
|
+
while (response.indexOf("\r\n\r\n") < 0) {
|
|
238
|
+
const chunk = await nextChunk(socket, timeoutMs);
|
|
239
|
+
response = Buffer.concat([response, chunk], Math.min(HEADER_LIMIT + 1, response.length + chunk.length));
|
|
240
|
+
if (response.length > HEADER_LIMIT)
|
|
241
|
+
throw proxyError("REPOST_PROXY_CONNECT_FAILED");
|
|
242
|
+
}
|
|
243
|
+
const boundary = response.indexOf("\r\n\r\n");
|
|
244
|
+
const status = Number(/^HTTP\/1\.1 ([0-9]{3})/u.exec(response.subarray(0, boundary).toString("latin1"))?.[1]);
|
|
245
|
+
if (!Number.isInteger(status))
|
|
246
|
+
throw proxyError("REPOST_PROXY_CONNECT_FAILED");
|
|
247
|
+
const remainder = response.subarray(boundary + 4);
|
|
248
|
+
if (remainder.length > 0)
|
|
249
|
+
socket.unshift(remainder);
|
|
250
|
+
return { socket, status };
|
|
251
|
+
}
|
|
252
|
+
function secureTunnel(socket, target, tls, allowH2, timeoutMs) {
|
|
253
|
+
const options = {
|
|
254
|
+
socket,
|
|
255
|
+
servername: target.servername ?? target.hostname,
|
|
256
|
+
ALPNProtocols: allowH2 ? ["h2", "http/1.1"] : ["http/1.1"],
|
|
257
|
+
ca: tls?.caCertificates ? [...tls.caCertificates] : undefined,
|
|
258
|
+
minVersion: tls?.minVersion ?? "TLSv1.2",
|
|
259
|
+
maxVersion: tls?.maxVersion,
|
|
260
|
+
ciphers: tls?.ciphers?.join(":"),
|
|
261
|
+
cert: tls?.clientCertificate?.certificate,
|
|
262
|
+
key: tls?.clientCertificate?.key,
|
|
263
|
+
passphrase: tls?.clientCertificate?.passphrase,
|
|
264
|
+
};
|
|
265
|
+
const secure = connect$1(options);
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
const timer = setTimeout(() => secure.destroy(proxyError("UND_ERR_CONNECT_TIMEOUT")), timeoutMs);
|
|
268
|
+
secure.once("secureConnect", () => { clearTimeout(timer); resolve(secure); });
|
|
269
|
+
secure.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function socketReady(socket, timeoutMs) {
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
const timer = setTimeout(() => socket.destroy(proxyError("UND_ERR_CONNECT_TIMEOUT")), timeoutMs);
|
|
275
|
+
socket.once("connect", () => { clearTimeout(timer); resolve(); });
|
|
276
|
+
socket.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function nextChunk(socket, timeoutMs) {
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
const cleanup = () => { clearTimeout(timer); socket.off("error", onError); socket.off("data", onData); socket.off("end", onEnd); };
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
cleanup();
|
|
284
|
+
socket.destroy();
|
|
285
|
+
reject(proxyError("UND_ERR_CONNECT_TIMEOUT"));
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
const onData = (chunk) => { cleanup(); socket.pause(); resolve(chunk); };
|
|
288
|
+
const onError = (error) => { cleanup(); reject(error); };
|
|
289
|
+
const onEnd = () => { cleanup(); reject(proxyError("REPOST_PROXY_CONNECT_FAILED")); };
|
|
290
|
+
socket.once("data", onData);
|
|
291
|
+
socket.once("error", onError);
|
|
292
|
+
socket.once("end", onEnd);
|
|
293
|
+
socket.resume();
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function proxyError(code, cause) {
|
|
297
|
+
const error = new Error("proxy connection failed");
|
|
298
|
+
error.code = code;
|
|
299
|
+
error.cause = cause;
|
|
300
|
+
return error;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function createH2Pool(origin, firstSocket, socketFactory) {
|
|
304
|
+
let initial = firstSocket;
|
|
305
|
+
let sessionPromise = null;
|
|
306
|
+
const session = () => {
|
|
307
|
+
if (sessionPromise)
|
|
308
|
+
return sessionPromise;
|
|
309
|
+
sessionPromise = (async () => {
|
|
310
|
+
const socket = initial ?? await socketFactory();
|
|
311
|
+
initial = null;
|
|
312
|
+
const client = connect$2(origin, { createConnection: () => socket });
|
|
313
|
+
client.once("goaway", () => { if (sessionPromise)
|
|
314
|
+
sessionPromise = null; });
|
|
315
|
+
client.once("close", () => { sessionPromise = null; });
|
|
316
|
+
await new Promise((resolve, reject) => {
|
|
317
|
+
client.once("connect", resolve);
|
|
318
|
+
client.once("error", reject);
|
|
319
|
+
});
|
|
320
|
+
return client;
|
|
321
|
+
})();
|
|
322
|
+
return sessionPromise;
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
async dispatch(request) {
|
|
326
|
+
const client = await session();
|
|
327
|
+
return dispatchH2(client, origin, request);
|
|
328
|
+
},
|
|
329
|
+
async close() {
|
|
330
|
+
const client = sessionPromise ? await sessionPromise.catch(() => null) : null;
|
|
331
|
+
client?.close();
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
function dispatchH2(client, origin, request) {
|
|
336
|
+
return new Promise((resolve, reject) => {
|
|
337
|
+
const authority = headerValue(request.headers, "host");
|
|
338
|
+
const headers = {
|
|
339
|
+
":method": "POST",
|
|
340
|
+
":scheme": new URL(origin).protocol.slice(0, -1),
|
|
341
|
+
":authority": authority,
|
|
342
|
+
":path": request.path,
|
|
343
|
+
};
|
|
344
|
+
for (let index = 0; index + 1 < request.headers.length; index += 2) {
|
|
345
|
+
const name = request.headers[index] ?? "";
|
|
346
|
+
if (name !== "host")
|
|
347
|
+
headers[name] = request.headers[index + 1] ?? "";
|
|
348
|
+
}
|
|
349
|
+
let stream;
|
|
350
|
+
try {
|
|
351
|
+
stream = client.request(headers, { endStream: false });
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
reject(error);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
let status = 0;
|
|
358
|
+
let rawHeaders = [];
|
|
359
|
+
const chunks = [];
|
|
360
|
+
let compressedBytes = 0;
|
|
361
|
+
let retainedBytes = 0;
|
|
362
|
+
let truncated = false;
|
|
363
|
+
let settled = false;
|
|
364
|
+
const cleanup = () => {
|
|
365
|
+
request.signal.removeEventListener("abort", abort);
|
|
366
|
+
client.removeListener("goaway", refusedByGoaway);
|
|
367
|
+
};
|
|
368
|
+
const finish = (response) => {
|
|
369
|
+
if (settled)
|
|
370
|
+
return;
|
|
371
|
+
settled = true;
|
|
372
|
+
cleanup();
|
|
373
|
+
resolve(response);
|
|
374
|
+
};
|
|
375
|
+
const fail = (error) => {
|
|
376
|
+
if (settled)
|
|
377
|
+
return;
|
|
378
|
+
settled = true;
|
|
379
|
+
cleanup();
|
|
380
|
+
reject(normalizeH2Error(error));
|
|
381
|
+
};
|
|
382
|
+
const abort = () => stream.close(constants.NGHTTP2_CANCEL);
|
|
383
|
+
const refusedByGoaway = (_errorCode, lastStreamId) => {
|
|
384
|
+
if (typeof stream.id === "number" && stream.id > lastStreamId) {
|
|
385
|
+
fail(Object.assign(new Error("HTTP/2 stream refused by GOAWAY"), { code: "REPOST_H2_REFUSED" }));
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
389
|
+
client.on("goaway", refusedByGoaway);
|
|
390
|
+
stream.once("response", (responseHeaders) => {
|
|
391
|
+
status = Number(responseHeaders[":status"] ?? 0);
|
|
392
|
+
rawHeaders = h2Headers(responseHeaders);
|
|
393
|
+
const declared = rawHeaderValue$1(rawHeaders, "content-length");
|
|
394
|
+
if (declared !== null && /^(?:0|[1-9][0-9]*)$/u.test(declared) && Number(declared) > request.maxResponseBytes) {
|
|
395
|
+
truncated = true;
|
|
396
|
+
stream.close(constants.NGHTTP2_CANCEL);
|
|
397
|
+
finish({ status, rawHeaders, chunks, compressedBytes, truncated });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
stream.on("data", (chunk) => {
|
|
401
|
+
compressedBytes += chunk.length;
|
|
402
|
+
const room = request.maxResponseBytes - retainedBytes;
|
|
403
|
+
if (room > 0) {
|
|
404
|
+
const retained = new Uint8Array(chunk.subarray(0, room));
|
|
405
|
+
chunks.push(retained);
|
|
406
|
+
retainedBytes += retained.length;
|
|
407
|
+
}
|
|
408
|
+
if (compressedBytes > request.maxResponseBytes) {
|
|
409
|
+
truncated = true;
|
|
410
|
+
stream.close(constants.NGHTTP2_CANCEL);
|
|
411
|
+
finish({ status, rawHeaders, chunks, compressedBytes, truncated });
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
stream.once("end", () => {
|
|
415
|
+
if (status === 0)
|
|
416
|
+
fail(Object.assign(new Error("HTTP/2 stream ended before response"), { code: "ECONNRESET" }));
|
|
417
|
+
else
|
|
418
|
+
finish({ status, rawHeaders, chunks, compressedBytes, truncated });
|
|
419
|
+
});
|
|
420
|
+
stream.once("error", fail);
|
|
421
|
+
stream.once("aborted", () => fail(Object.assign(new Error("HTTP/2 stream reset"), { code: "ECONNRESET" })));
|
|
422
|
+
if (request.signal.aborted) {
|
|
423
|
+
abort();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
stream.end(request.body, () => {
|
|
427
|
+
if (stream.rstCode === 0)
|
|
428
|
+
request.onBodySent();
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function h2Headers(headers) {
|
|
433
|
+
return Object.entries(headers).flatMap(([name, values]) => {
|
|
434
|
+
if (name.startsWith(":") || values === undefined)
|
|
435
|
+
return [];
|
|
436
|
+
return (Array.isArray(values) ? values : [String(values)]).flatMap((value) => [Buffer.from(name, "ascii"), Buffer.from(value, "utf8")]);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function headerValue(headers, name) {
|
|
440
|
+
for (let index = 0; index + 1 < headers.length; index += 2)
|
|
441
|
+
if (headers[index] === name)
|
|
442
|
+
return headers[index + 1] ?? "";
|
|
443
|
+
return "";
|
|
444
|
+
}
|
|
445
|
+
function rawHeaderValue$1(raw, name) {
|
|
446
|
+
for (let index = 0; index + 1 < raw.length; index += 2) {
|
|
447
|
+
if (Buffer.from(raw[index]).toString("ascii").toLowerCase() === name)
|
|
448
|
+
return Buffer.from(raw[index + 1]).toString("utf8");
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
function normalizeH2Error(error) {
|
|
453
|
+
if (error && typeof error === "object" && error.code === "ERR_HTTP2_STREAM_ERROR") {
|
|
454
|
+
error.code = "ECONNRESET";
|
|
455
|
+
}
|
|
456
|
+
return error;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function openOriginSocket(origin, options) {
|
|
460
|
+
const url = new URL(origin);
|
|
461
|
+
const hostname = url.hostname.replace(/^\[|\]$/gu, "");
|
|
462
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
463
|
+
if (options.proxy) {
|
|
464
|
+
return connectThroughProxy(options.proxy, { hostname, port, protocol: url.protocol, servername: hostname }, options.tls, options.allowH2, options.connectTimeout);
|
|
465
|
+
}
|
|
466
|
+
const resolved = options.dnsResolver ? await resolveAddress(options.dnsResolver, hostname) : null;
|
|
467
|
+
const host = resolved?.address ?? hostname;
|
|
468
|
+
if (url.protocol === "http:") {
|
|
469
|
+
return awaitConnected(connect({ host, port: Number(port), family: resolved?.family }), options.connectTimeout);
|
|
470
|
+
}
|
|
471
|
+
const tlsOptions = {
|
|
472
|
+
host,
|
|
473
|
+
port: Number(port),
|
|
474
|
+
servername: isIP(hostname) === 0 ? hostname : undefined,
|
|
475
|
+
ALPNProtocols: options.allowH2 ? ["h2", "http/1.1"] : ["http/1.1"],
|
|
476
|
+
ca: options.tls?.caCertificates ? [...options.tls.caCertificates] : undefined,
|
|
477
|
+
minVersion: options.tls?.minVersion ?? "TLSv1.2",
|
|
478
|
+
maxVersion: options.tls?.maxVersion,
|
|
479
|
+
ciphers: options.tls?.ciphers?.join(":"),
|
|
480
|
+
cert: options.tls?.clientCertificate?.certificate,
|
|
481
|
+
key: options.tls?.clientCertificate?.key,
|
|
482
|
+
passphrase: options.tls?.clientCertificate?.passphrase,
|
|
483
|
+
};
|
|
484
|
+
return awaitSecure(connect$1(tlsOptions), options.connectTimeout);
|
|
485
|
+
}
|
|
486
|
+
async function resolveAddress(resolver, hostname) {
|
|
487
|
+
try {
|
|
488
|
+
const addresses = await resolver(hostname);
|
|
489
|
+
const address = addresses[0];
|
|
490
|
+
if (!address || (address.family !== 4 && address.family !== 6))
|
|
491
|
+
throw new Error("empty DNS result");
|
|
492
|
+
return address;
|
|
493
|
+
}
|
|
494
|
+
catch (cause) {
|
|
495
|
+
const error = new Error("DNS resolver failed");
|
|
496
|
+
error.code = "REPOST_DNS_RESOLVER";
|
|
497
|
+
error.cause = cause;
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
function awaitConnected(socket, timeoutMs) {
|
|
502
|
+
return new Promise((resolve, reject) => {
|
|
503
|
+
const timer = setTimeout(() => socket.destroy(timeoutError()), timeoutMs);
|
|
504
|
+
socket.once("connect", () => { clearTimeout(timer); resolve(socket); });
|
|
505
|
+
socket.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function awaitSecure(socket, timeoutMs) {
|
|
509
|
+
return new Promise((resolve, reject) => {
|
|
510
|
+
const timer = setTimeout(() => socket.destroy(timeoutError()), timeoutMs);
|
|
511
|
+
socket.once("secureConnect", () => { clearTimeout(timer); resolve(socket); });
|
|
512
|
+
socket.once("error", (error) => { clearTimeout(timer); reject(error); });
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function timeoutError() {
|
|
516
|
+
const error = new Error("connection timed out");
|
|
517
|
+
error.code = "UND_ERR_CONNECT_TIMEOUT";
|
|
518
|
+
return error;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** The sole Undici compatibility boundary for the SDK. */
|
|
522
|
+
function createUndiciPool(origin, options) {
|
|
523
|
+
if (!options.allowH2 || !origin.startsWith("https://"))
|
|
524
|
+
return createHttp1Pool(origin, options);
|
|
525
|
+
let lanePromise = null;
|
|
526
|
+
const lane = () => {
|
|
527
|
+
lanePromise ?? (lanePromise = (async () => {
|
|
528
|
+
const socket = await openOriginSocket(origin, options);
|
|
529
|
+
if ("alpnProtocol" in socket && socket.alpnProtocol === "h2") {
|
|
530
|
+
return createH2Pool(origin, socket, async () => {
|
|
531
|
+
const next = await openOriginSocket(origin, options);
|
|
532
|
+
if (!("alpnProtocol" in next) || next.alpnProtocol !== "h2") {
|
|
533
|
+
next.destroy();
|
|
534
|
+
throw Object.assign(new Error("HTTP/2 negotiation failed"), { code: "ERR_TLS_ALPN_NEGOTIATION" });
|
|
535
|
+
}
|
|
536
|
+
return next;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return createHttp1Pool(origin, { ...options, allowH2: false }, socket);
|
|
540
|
+
})());
|
|
541
|
+
return lanePromise;
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
async dispatch(request) { return (await lane()).dispatch(request); },
|
|
545
|
+
async close() { if (lanePromise)
|
|
546
|
+
await (await lanePromise).close(); },
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function createHttp1Pool(origin, options, initialSocket) {
|
|
550
|
+
const bodyCommits = new WeakMap();
|
|
551
|
+
const lookup = options.dnsResolver
|
|
552
|
+
? (hostname, lookupOptions, callback) => {
|
|
553
|
+
options.dnsResolver?.(hostname).then((addresses) => callback(null, lookupOptions.all ? addresses : addresses[0]?.address ?? "", addresses[0]?.family), (error) => callback(dnsResolverError(error), "", 0));
|
|
554
|
+
}
|
|
555
|
+
: undefined;
|
|
556
|
+
const connector = buildConnector({
|
|
557
|
+
allowH2: options.allowH2,
|
|
558
|
+
timeout: options.connectTimeout,
|
|
559
|
+
lookup,
|
|
560
|
+
ca: options.tls?.caCertificates ? [...options.tls.caCertificates] : undefined,
|
|
561
|
+
minVersion: options.tls?.minVersion ?? "TLSv1.2",
|
|
562
|
+
maxVersion: options.tls?.maxVersion,
|
|
563
|
+
ciphers: options.tls?.ciphers?.join(":"),
|
|
564
|
+
cert: options.tls?.clientCertificate?.certificate,
|
|
565
|
+
key: options.tls?.clientCertificate?.key,
|
|
566
|
+
passphrase: options.tls?.clientCertificate?.passphrase,
|
|
567
|
+
});
|
|
568
|
+
const connect = (connectOptions, callback) => {
|
|
569
|
+
if (initialSocket) {
|
|
570
|
+
const socket = initialSocket;
|
|
571
|
+
initialSocket = undefined;
|
|
572
|
+
if (!("alpnProtocol" in socket) || socket.alpnProtocol !== "h2")
|
|
573
|
+
installExactH1Framing(socket, bodyCommits);
|
|
574
|
+
queueMicrotask(() => callback(null, socket));
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (options.proxy) {
|
|
578
|
+
connectThroughProxy(options.proxy, connectOptions, options.tls, options.allowH2, options.connectTimeout).then((socket) => {
|
|
579
|
+
if (!("alpnProtocol" in socket) || socket.alpnProtocol !== "h2")
|
|
580
|
+
installExactH1Framing(socket, bodyCommits);
|
|
581
|
+
callback(null, socket);
|
|
582
|
+
}, (error) => callback(error, null));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
connector(connectOptions, (error, socket) => {
|
|
586
|
+
if (error) {
|
|
587
|
+
callback(error, null);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (!("alpnProtocol" in socket) || socket.alpnProtocol !== "h2")
|
|
591
|
+
installExactH1Framing(socket, bodyCommits);
|
|
592
|
+
callback(null, socket);
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
const pool = new Pool(origin, {
|
|
596
|
+
connections: options.connections,
|
|
597
|
+
clientTtl: options.clientTtl,
|
|
598
|
+
keepAliveTimeout: options.keepAliveTimeout,
|
|
599
|
+
keepAliveMaxTimeout: options.keepAliveTimeout,
|
|
600
|
+
allowH2: options.allowH2,
|
|
601
|
+
pipelining: 1,
|
|
602
|
+
strictContentLength: true,
|
|
603
|
+
maxHeaderSize: 131072,
|
|
604
|
+
connect,
|
|
605
|
+
});
|
|
606
|
+
return {
|
|
607
|
+
dispatch(request) {
|
|
608
|
+
return new Promise((resolve, reject) => {
|
|
609
|
+
let bodySent = false;
|
|
610
|
+
const bodyKey = request.body.buffer;
|
|
611
|
+
const bodyCommit = { deferred: false, commit: request.onBodySent };
|
|
612
|
+
bodyCommits.set(bodyKey, bodyCommit);
|
|
613
|
+
let status = 0;
|
|
614
|
+
let rawHeaders = [];
|
|
615
|
+
const chunks = [];
|
|
616
|
+
let compressedBytes = 0;
|
|
617
|
+
let retainedBytes = 0;
|
|
618
|
+
let truncated = false;
|
|
619
|
+
let abortDispatch;
|
|
620
|
+
const limitError = new Error("response body limit exceeded");
|
|
621
|
+
const abortRequest = () => abortDispatch?.(request.signal.reason instanceof Error ? request.signal.reason : new Error("request aborted"));
|
|
622
|
+
request.signal.addEventListener("abort", abortRequest, { once: true });
|
|
623
|
+
pool.dispatch({
|
|
624
|
+
path: request.path,
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: request.headers,
|
|
627
|
+
body: request.body,
|
|
628
|
+
idempotent: true,
|
|
629
|
+
blocking: false,
|
|
630
|
+
headersTimeout: request.headersTimeout,
|
|
631
|
+
bodyTimeout: request.bodyTimeout,
|
|
632
|
+
}, {
|
|
633
|
+
onConnect(abort) {
|
|
634
|
+
abortDispatch = abort;
|
|
635
|
+
if (request.signal.aborted)
|
|
636
|
+
abortRequest();
|
|
637
|
+
},
|
|
638
|
+
onBodySent() {
|
|
639
|
+
if (bodySent)
|
|
640
|
+
return;
|
|
641
|
+
bodySent = true;
|
|
642
|
+
if (!bodyCommit.deferred) {
|
|
643
|
+
bodyCommits.delete(bodyKey);
|
|
644
|
+
request.onBodySent();
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
onHeaders(statusCode, headers) {
|
|
648
|
+
status = statusCode;
|
|
649
|
+
rawHeaders = headers.map((value) => new Uint8Array(value));
|
|
650
|
+
const declared = rawHeaderValue(rawHeaders, "content-length");
|
|
651
|
+
if (declared !== null && /^(?:0|[1-9][0-9]*)$/u.test(declared) && Number(declared) > request.maxResponseBytes) {
|
|
652
|
+
truncated = true;
|
|
653
|
+
abortDispatch?.(limitError);
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
656
|
+
},
|
|
657
|
+
onData(chunk) {
|
|
658
|
+
compressedBytes += chunk.length;
|
|
659
|
+
const room = request.maxResponseBytes - retainedBytes;
|
|
660
|
+
if (room > 0) {
|
|
661
|
+
const retained = new Uint8Array(chunk.subarray(0, room));
|
|
662
|
+
chunks.push(retained);
|
|
663
|
+
retainedBytes += retained.length;
|
|
664
|
+
}
|
|
665
|
+
if (compressedBytes > request.maxResponseBytes) {
|
|
666
|
+
truncated = true;
|
|
667
|
+
abortDispatch?.(limitError);
|
|
668
|
+
}
|
|
669
|
+
return true;
|
|
670
|
+
},
|
|
671
|
+
onComplete() {
|
|
672
|
+
request.signal.removeEventListener("abort", abortRequest);
|
|
673
|
+
bodyCommits.delete(bodyKey);
|
|
674
|
+
resolve({ status, rawHeaders, chunks, compressedBytes, truncated });
|
|
675
|
+
},
|
|
676
|
+
onError(error) {
|
|
677
|
+
request.signal.removeEventListener("abort", abortRequest);
|
|
678
|
+
bodyCommits.delete(bodyKey);
|
|
679
|
+
if (error === limitError)
|
|
680
|
+
resolve({ status, rawHeaders, chunks, compressedBytes, truncated: true });
|
|
681
|
+
else if (status !== 0)
|
|
682
|
+
resolve({
|
|
683
|
+
status,
|
|
684
|
+
rawHeaders,
|
|
685
|
+
chunks,
|
|
686
|
+
compressedBytes,
|
|
687
|
+
truncated: status < 200 || status >= 300,
|
|
688
|
+
});
|
|
689
|
+
else
|
|
690
|
+
reject(error);
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
close: () => pool.close(),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function dnsResolverError(cause) {
|
|
699
|
+
const error = new Error("DNS resolver failed");
|
|
700
|
+
error.code = "REPOST_DNS_RESOLVER";
|
|
701
|
+
error.cause = cause;
|
|
702
|
+
return error;
|
|
703
|
+
}
|
|
704
|
+
function rawHeaderValue(raw, target) {
|
|
705
|
+
let value = null;
|
|
706
|
+
for (let index = 0; index + 1 < raw.length; index += 2) {
|
|
707
|
+
if (Buffer.from(raw[index]).toString("latin1").toLowerCase() !== target)
|
|
708
|
+
continue;
|
|
709
|
+
if (value !== null)
|
|
710
|
+
return null;
|
|
711
|
+
value = Buffer.from(raw[index + 1]).toString("latin1");
|
|
712
|
+
}
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
715
|
+
function installExactH1Framing(socket, bodyCommits) {
|
|
716
|
+
const write = socket.write.bind(socket);
|
|
717
|
+
let writeChain = Promise.resolve();
|
|
718
|
+
socket.write = ((chunk, encoding, callback) => {
|
|
719
|
+
if (typeof chunk === "string" && chunk.startsWith("POST ") && chunk.includes(" HTTP/1.1\r\n")) {
|
|
720
|
+
chunk = normalizeH1Header(chunk);
|
|
721
|
+
writeChain = writeChain.then(() => new Promise((resolve) => {
|
|
722
|
+
if (socket.destroyed)
|
|
723
|
+
return resolve();
|
|
724
|
+
write(chunk, encoding, (error) => { callback?.(error); resolve(); });
|
|
725
|
+
}));
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
if (chunk instanceof Uint8Array && chunk.byteLength > 0) {
|
|
729
|
+
const bodyCommit = bodyCommits.get(chunk.buffer);
|
|
730
|
+
if (bodyCommit) {
|
|
731
|
+
bodyCommit.deferred = true;
|
|
732
|
+
writeChain = writeChain
|
|
733
|
+
.then(() => peerResetOpportunity())
|
|
734
|
+
.then(() => new Promise((resolve) => {
|
|
735
|
+
if (socket.destroyed)
|
|
736
|
+
return resolve();
|
|
737
|
+
write(chunk, encoding, (error) => {
|
|
738
|
+
bodyCommits.delete(chunk.buffer);
|
|
739
|
+
if (!error)
|
|
740
|
+
bodyCommit.commit();
|
|
741
|
+
callback?.(error);
|
|
742
|
+
resolve();
|
|
743
|
+
});
|
|
744
|
+
}));
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return write(chunk, encoding, callback);
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
function peerResetOpportunity() {
|
|
752
|
+
// Let a peer that rejected the headers close before any body byte is queued.
|
|
753
|
+
// This keeps a header-only reset classified as uncommitted.
|
|
754
|
+
return new Promise((resolve) => setTimeout(resolve, 5));
|
|
755
|
+
}
|
|
756
|
+
function normalizeH1Header(header) {
|
|
757
|
+
const withoutConnection = header.replace(/^connection: (?:keep-alive|close)\r\n/mu, "");
|
|
758
|
+
const contentLength = /^content-length: [0-9]+\r\n/mu.exec(withoutConnection)?.[0];
|
|
759
|
+
if (!contentLength)
|
|
760
|
+
return withoutConnection;
|
|
761
|
+
const withoutLength = withoutConnection.replace(contentLength, "");
|
|
762
|
+
return withoutLength.replace(/^(host: [^\r\n]+\r\n)/mu, `$1${contentLength}`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const RESPONSE_LIMIT_BYTES = 1048576;
|
|
766
|
+
function createNodeTransport(options = {}) {
|
|
767
|
+
const compressedLimit = options.responseCompressedLimitBytes ?? RESPONSE_LIMIT_BYTES;
|
|
768
|
+
const pools = new Map();
|
|
769
|
+
const poolFor = (origin, request) => {
|
|
770
|
+
let pool = pools.get(origin);
|
|
771
|
+
if (!pool) {
|
|
772
|
+
pool = createUndiciPool(origin, {
|
|
773
|
+
connections: options.maxConnectionsPerOrigin ?? 32,
|
|
774
|
+
clientTtl: options.connectionLifetimeMs ?? 300000,
|
|
775
|
+
keepAliveTimeout: options.connectionIdleTimeoutMs ?? 120000,
|
|
776
|
+
allowH2: options.http2 ?? true,
|
|
777
|
+
connectTimeout: request.connectTimeoutMs,
|
|
778
|
+
tls: options.tls,
|
|
779
|
+
dnsResolver: options.dnsResolver,
|
|
780
|
+
proxy: options.proxy,
|
|
781
|
+
});
|
|
782
|
+
pools.set(origin, pool);
|
|
783
|
+
}
|
|
784
|
+
return pool;
|
|
785
|
+
};
|
|
786
|
+
return {
|
|
787
|
+
async send(request) {
|
|
788
|
+
const framed = frameRequest(request);
|
|
789
|
+
const pool = poolFor(framed.origin, request);
|
|
790
|
+
try {
|
|
791
|
+
const response = await dispatch(pool, framed.path, framed.headers, request, compressedLimit);
|
|
792
|
+
return await readResponse(response.status, response.rawHeaders, response.chunks, response.compressedBytes, response.truncated, compressedLimit);
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
return classifyFailure(error, request.commitTracker.committed);
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
async close() {
|
|
799
|
+
await Promise.all([...pools.values()].map((pool) => pool.close()));
|
|
800
|
+
pools.clear();
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function dispatch(pool, path, headers, request, compressedLimit) {
|
|
805
|
+
return pool.dispatch({
|
|
806
|
+
path,
|
|
807
|
+
headers,
|
|
808
|
+
body: request.bodyBytes,
|
|
809
|
+
headersTimeout: request.attemptTimeoutMs,
|
|
810
|
+
bodyTimeout: request.attemptTimeoutMs,
|
|
811
|
+
signal: request.signal,
|
|
812
|
+
onBodySent: () => request.commitTracker.markCommitted(),
|
|
813
|
+
maxResponseBytes: compressedLimit + 1,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function classifyFailure(error, committed) {
|
|
817
|
+
if (errorCode$1(error) === "REPOST_H2_REFUSED") {
|
|
818
|
+
return { kind: "failure", code: "IO", reason: "CONNECTION_RESET", committed: false };
|
|
819
|
+
}
|
|
820
|
+
const classified = classifyNetworkFailure(error, committed);
|
|
821
|
+
if (classified)
|
|
822
|
+
return classified;
|
|
823
|
+
if (errorCode$1(error) === "ECONNRESET" || errorCode$1(error) === "ERR_HTTP2_STREAM_ERROR") {
|
|
824
|
+
return { kind: "failure", code: "IO", reason: "CONNECTION_RESET", committed };
|
|
825
|
+
}
|
|
826
|
+
if (errorCode$1(error) === "UND_ERR_INFO" && error instanceof Error && error.message.startsWith("HTTP/2:")) {
|
|
827
|
+
return { kind: "failure", code: "IO", reason: "CONNECTION_RESET", committed };
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
kind: "failure",
|
|
831
|
+
code: "IO",
|
|
832
|
+
reason: "UNKNOWN",
|
|
833
|
+
committed,
|
|
834
|
+
causeCategory: "HTTP_RUNTIME",
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export { createNodeTransport };
|
|
839
|
+
//# sourceMappingURL=node-BAJt_qhB.mjs.map
|