tunnelfetch 1.6.5 → 1.8.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 +64 -8
- package/package.json +1 -1
- package/src/client/decode.js +52 -8
- package/src/client.js +27 -6
- package/src/tls/connect.js +1 -0
- package/src/tls/record.js +1 -1
- package/src/util/bytes.js +47 -3
- package/types/client.d.ts +11 -3
- package/types/util/bytes.d.ts +9 -2
package/README.md
CHANGED
|
@@ -716,14 +716,27 @@ Fetching a size-controlled origin through a proxy, warm, medians over seven-plus
|
|
|
716
716
|
isolate, gzip on the wire. The last column is the same numbers as a rate, which is the form worth
|
|
717
717
|
carrying around:
|
|
718
718
|
|
|
719
|
-
| Body |
|
|
720
|
-
| --- | --- | --- |
|
|
721
|
-
| 1 KB |
|
|
722
|
-
| 16 KB |
|
|
723
|
-
| 64 KB |
|
|
724
|
-
| 256 KB |
|
|
725
|
-
| 1 MB |
|
|
726
|
-
| 4 MB |
|
|
719
|
+
| Body | Per request, reusing a connection | Per decompressed MB |
|
|
720
|
+
| --- | --- | --- |
|
|
721
|
+
| 1 KB | **0.5 ms** | — |
|
|
722
|
+
| 16 KB | **3.5 ms** | 224 ms/MB |
|
|
723
|
+
| 64 KB | **7.5 ms** | 120 ms/MB |
|
|
724
|
+
| 256 KB | **20.5 ms** | 82 ms/MB |
|
|
725
|
+
| 1 MB | **51.5 ms** | 51 ms/MB |
|
|
726
|
+
| 4 MB | **104 ms** | 26 ms/MB |
|
|
727
|
+
|
|
728
|
+
Opening a connection adds **7–12 ms** on top, once, however many requests follow it.
|
|
729
|
+
|
|
730
|
+
**Read the per-MB column before doing any arithmetic with this table.** It falls 8.6x from end to
|
|
731
|
+
end, so there is no such thing as a per-MB rate for this package. A least-squares line through these
|
|
732
|
+
points is `9.17 + 24.86 x MB`, which predicts 9.17 ms for a 1 KB body against a measured 0.5 — wrong
|
|
733
|
+
by 18x. Any budget built on a single per-MB figure will be badly wrong at one end or the other.
|
|
734
|
+
|
|
735
|
+
The reason is that V8 tiers up **inside a single request**. A 1 MB body runs the decode loop
|
|
736
|
+
interpreted the whole way; a 4 MB body pays that for its first megabyte and runs the rest optimised.
|
|
737
|
+
`51.5 + 3 x 17.5 = 104` fits, so the steady-state cost is about **17.5 ms/MB with a ~51 ms entry fee
|
|
738
|
+
per request**. For a size not in the table, interpolate within it rather than extrapolating from a
|
|
739
|
+
rate.
|
|
727
740
|
|
|
728
741
|
The cold-start cost is a **total**, not something to add to a row above:
|
|
729
742
|
|
|
@@ -924,6 +937,49 @@ the edge: receiving a 4 MB body **uncompressed** costs the same as receiving the
|
|
|
924
937
|
the bytes through the entire receive pipeline. **Leave compression on.** The fixed ~2 ms only wins
|
|
925
938
|
below roughly the size where a single wire read covers the whole body.
|
|
926
939
|
|
|
940
|
+
### When not to use this package
|
|
941
|
+
|
|
942
|
+
**If you can run Node, run Node — and then you do not need this package at all.** `undici` with a
|
|
943
|
+
`ProxyAgent` does the same job over `node:tls`, which verifies whatever hostname you name, in C, at
|
|
944
|
+
roughly a tenth of the CPU. This package exists because a V8 isolate cannot do that; it is not a
|
|
945
|
+
better way to do it.
|
|
946
|
+
|
|
947
|
+
The arithmetic is worth being blunt about. A billion 4 MB requests a month costs about **$2,385** of
|
|
948
|
+
Workers CPU. That workload is roughly 386 requests a second and 4.5 Gbps sustained — **three
|
|
949
|
+
dedicated boxes** at Hetzner-class pricing carry it for around **$600**. So for large bodies, buying
|
|
950
|
+
servers is about **four times cheaper**, and the gap widens with body size.
|
|
951
|
+
|
|
952
|
+
Two things flip that back:
|
|
953
|
+
|
|
954
|
+
* **Egress.** Workers does not bill bandwidth. If those bytes have to leave your own servers again,
|
|
955
|
+
1.45 PB/month is free on an unmetered box and about **$130,000** on AWS. Price your transfer
|
|
956
|
+
before pricing your CPU — it can dwarf everything above.
|
|
957
|
+
* **Body size.** Under roughly **128 KB** the per-request cost is small enough that Workers wins on
|
|
958
|
+
price *and* gives you 300+ locations, no operations and no capacity planning. At 16 KB the same
|
|
959
|
+
billion requests is **$375**.
|
|
960
|
+
|
|
961
|
+
So: small bodies at the edge is what this package is for. Large bodies through a proxy, from a
|
|
962
|
+
runtime that could have used native TLS, is the case where it is the wrong tool and the bill says so.
|
|
963
|
+
|
|
964
|
+
### Passing the body through instead of decoding it
|
|
965
|
+
|
|
966
|
+
If you are forwarding a response rather than reading it, `decompress: 'passthrough'` asks for gzip as
|
|
967
|
+
usual and hands back the **coded** bytes with their `Content-Encoding` and `Content-Length` intact,
|
|
968
|
+
so the next hop can relay them:
|
|
969
|
+
|
|
970
|
+
```js
|
|
971
|
+
new Client({ connect, proxy, decompress: 'passthrough' });
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
Measured end to end on a 4 MB body: **118 ms decoding against 82.5 ms passing through, 30% cheaper**,
|
|
975
|
+
with 2.76x fewer bytes on the wire at the same time.
|
|
976
|
+
|
|
977
|
+
**This only helps if you never need the plaintext.** If you parse, extract or transform the body, the
|
|
978
|
+
decode is work you owe — passthrough just moves it downstream, where you or your caller pays the same.
|
|
979
|
+
It is not a general optimisation. Note also that `decompress: false` is *not* a weaker version of
|
|
980
|
+
this: it also drops gzip from `Accept-Encoding`, so the origin sends plaintext and the wire grows
|
|
981
|
+
2.76x. It loses on both sides.
|
|
982
|
+
|
|
927
983
|
### Cost parity with the platform's `fetch` is not reachable, and here is the floor
|
|
928
984
|
|
|
929
985
|
Two independent investigations reached this separately, which is the main reason it is stated this
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "A fetch-shaped HTTP client that can route through HTTP CONNECT / HTTPS / SOCKS5 proxies on runtimes with only raw TCP, such as Cloudflare Workers. Implements TLS in userland because the runtime cannot verify a tunnelled peer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
package/src/client/decode.js
CHANGED
|
@@ -190,17 +190,53 @@ function decompressionStage(source, coding, maxBytes = Infinity) {
|
|
|
190
190
|
})();
|
|
191
191
|
ready.catch(() => {});
|
|
192
192
|
|
|
193
|
+
// The fast path: hand the decompressor's output to the runtime and never touch a chunk in JS.
|
|
194
|
+
//
|
|
195
|
+
// Measured on the edge, CPU per MB of decompressed output, all shapes interleaved in one isolate:
|
|
196
|
+
//
|
|
197
|
+
// this wrapper, BYOB reads + counting 7.00
|
|
198
|
+
// the same wrapper with counting removed 7.33 (counting is free; the WRAPPER is not)
|
|
199
|
+
// a standard `new TransformStream()` hop 13.33 (JS-backed: every 4 KiB chunk queues)
|
|
200
|
+
// an IdentityTransformStream hop 3.67 (native, and BYOB on its readable)
|
|
201
|
+
// DecompressionStream + native collect 3.33 (the floor)
|
|
202
|
+
//
|
|
203
|
+
// So a native identity hop is 48% cheaper than this wrapper and lands within noise of the floor,
|
|
204
|
+
// while the standard TransformStream — the obvious way to write the same thing — is nearly twice
|
|
205
|
+
// as expensive as doing nothing at all. The difference is that one is C++ and one is JavaScript;
|
|
206
|
+
// nothing about the API surface says so.
|
|
207
|
+
//
|
|
208
|
+
// It applies only when there is no cap, and that is not a limitation to be worked around: the cap
|
|
209
|
+
// is enforced by counting bytes, counting requires seeing them in JS, and seeing them in JS is
|
|
210
|
+
// exactly the cost being removed. The two are mutually exclusive. `maxBodyBytes: Infinity` is
|
|
211
|
+
// already a caller saying they will bound the body themselves, so giving that caller the fast
|
|
212
|
+
// path is coherent — you gave up the guard, you get the speed — rather than a compromise.
|
|
213
|
+
if (maxBytes === Infinity && typeof globalThis.IdentityTransformStream === 'function') {
|
|
214
|
+
const relay = new globalThis.IdentityTransformStream();
|
|
215
|
+
(async () => {
|
|
216
|
+
const ds = await ready;
|
|
217
|
+
if (ds === null) {
|
|
218
|
+
await relay.writable.close().catch(() => {});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// preventAbort: pipeTo's default is to abort the destination with the SOURCE's error, which
|
|
222
|
+
// would hand the consumer a bare zlib message from a stream it never asked about. Keeping
|
|
223
|
+
// the abort here is what lets the coding be named, the same as on the wrapper path.
|
|
224
|
+
await ds.readable.pipeTo(relay.writable, { preventAbort: true });
|
|
225
|
+
if (pumpDone) await pumpDone;
|
|
226
|
+
await relay.writable.close();
|
|
227
|
+
})().catch(async (e) => {
|
|
228
|
+
// The consumer sees the same typed error it would have seen through the wrapper; the relay is
|
|
229
|
+
// errored rather than closed so a truncated body can never read as a complete one.
|
|
230
|
+
await srcReader.cancel(wrapCoding(coding, e)).catch(() => {});
|
|
231
|
+
await relay.writable.abort(wrapCoding(coding, e)).catch(() => {});
|
|
232
|
+
});
|
|
233
|
+
return relay.readable;
|
|
234
|
+
}
|
|
235
|
+
|
|
193
236
|
/** @type {ReadableStreamBYOBReader | ReadableStreamDefaultReader<Uint8Array> | null} */
|
|
194
237
|
let out = null;
|
|
195
238
|
let byob = false;
|
|
196
|
-
const wrap = (e) =>
|
|
197
|
-
e instanceof HttpError
|
|
198
|
-
? e
|
|
199
|
-
: new HttpError(
|
|
200
|
-
codes.HTTP_CONTENT_ENCODING,
|
|
201
|
-
`decoding "${coding}" failed: ${e?.message ?? e}`,
|
|
202
|
-
{ coding },
|
|
203
|
-
);
|
|
239
|
+
const wrap = (e) => wrapCoding(coding, e);
|
|
204
240
|
|
|
205
241
|
return new ReadableStream({
|
|
206
242
|
async pull(c) {
|
|
@@ -334,6 +370,14 @@ function capDecodedOutput(source, coding, maxBytes) {
|
|
|
334
370
|
});
|
|
335
371
|
}
|
|
336
372
|
|
|
373
|
+
/** Name the coding in a decode failure, so a caller sees which one misbehaved rather than a bare
|
|
374
|
+
* zlib message from somewhere downstream. */
|
|
375
|
+
function wrapCoding(coding, e) {
|
|
376
|
+
return e instanceof HttpError
|
|
377
|
+
? e
|
|
378
|
+
: new HttpError(codes.HTTP_CONTENT_ENCODING, `decoding "${coding}" failed: ${e?.message ?? e}`, { coding });
|
|
379
|
+
}
|
|
380
|
+
|
|
337
381
|
/** Byte at logical offset `i` across the buffered head chunks. */
|
|
338
382
|
function firstBytes(chunks, i) {
|
|
339
383
|
for (const c of chunks) {
|
package/src/client.js
CHANGED
|
@@ -98,7 +98,11 @@ const DEFAULT_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
|
98
98
|
* again on the DECODED output — a compressed body within the cap can decompress far past it, and
|
|
99
99
|
* a registered decoder's output is bounded by it too. Pass `Infinity` to opt out, which is the
|
|
100
100
|
* right choice for streaming large files and the wrong one for fetching URLs you do not control.
|
|
101
|
-
* @property {boolean} [decompress] gzip/deflate. Default true.
|
|
101
|
+
* @property {boolean | 'passthrough'} [decompress] gzip/deflate. Default true. `false` neither
|
|
102
|
+
* advertises nor decodes a coding, so the origin sends plaintext and the wire grows.
|
|
103
|
+
* `'passthrough'` advertises gzip as usual but hands the CODED body back with its
|
|
104
|
+
* `Content-Encoding` intact — 30% cheaper on a 4 MB body and 2.76x fewer wire bytes, and correct
|
|
105
|
+
* only for a caller that forwards the body rather than reading it.
|
|
102
106
|
* @property {Record<string, import('./client/decode.js').BodyDecoder>} [decoders] extra
|
|
103
107
|
* content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
|
|
104
108
|
* makes advertising it honest, so each name is appended to Accept-Encoding — a client that
|
|
@@ -744,7 +748,7 @@ async function sendAndReceive(client, conn, current, { key, deadlines, reused })
|
|
|
744
748
|
deadlines.beginIdle();
|
|
745
749
|
const guarded = framing.kind === 'none' ? raw : withIdleDeadline(raw, deadlines);
|
|
746
750
|
const decoded = decodeResponseBody(guarded, headInfo.headers, o);
|
|
747
|
-
return buildResponse(headInfo, decoded, framing, conn);
|
|
751
|
+
return buildResponse(headInfo, decoded, framing, conn, o.decompress === 'passthrough');
|
|
748
752
|
}
|
|
749
753
|
|
|
750
754
|
// ------------------------------------------------------------------ one request/response over h2
|
|
@@ -801,7 +805,7 @@ async function sendAndReceiveH2(client, h2, current, { deadlines }) {
|
|
|
801
805
|
const guarded = withIdleDeadline(raw, deadlines);
|
|
802
806
|
const decoded = decodeResponseBody(guarded, head.headers, o);
|
|
803
807
|
const framing = { kind: 'h2', keepAliveEligible: false };
|
|
804
|
-
return buildResponse(head, decoded, framing, h2);
|
|
808
|
+
return buildResponse(head, decoded, framing, h2, client.options.decompress === 'passthrough');
|
|
805
809
|
}
|
|
806
810
|
|
|
807
811
|
/**
|
|
@@ -859,22 +863,39 @@ function wantsKeepAlive(headInfo) {
|
|
|
859
863
|
}
|
|
860
864
|
|
|
861
865
|
function decodeResponseBody(body, headers, options) {
|
|
862
|
-
|
|
866
|
+
// `false` means "do not ask for a coding and do not decode one". `'passthrough'` means "ask for
|
|
867
|
+
// gzip as usual, then hand the CODED bytes back with their Content-Encoding intact".
|
|
868
|
+
//
|
|
869
|
+
// The two are not variations on each other. `false` also drops gzip from Accept-Encoding, so the
|
|
870
|
+
// origin sends plaintext and the wire grows 2.76x on typical content — it loses on both sides.
|
|
871
|
+
// Passthrough is the combination a forwarding proxy actually wants, and until now it could only
|
|
872
|
+
// be assembled by setting `decompress: false` and then writing the Accept-Encoding header back on
|
|
873
|
+
// by hand, per request, which nothing documented.
|
|
874
|
+
//
|
|
875
|
+
// Measured end to end on a 4 MB body through a proxy: 118 ms decoding, 82.5 ms passing through —
|
|
876
|
+
// 30% — and 2.76x fewer bytes on the wire at the same time. But it only helps a caller who never
|
|
877
|
+
// needs the plaintext: if you parse the body, the decode is work you owe, not overhead, and
|
|
878
|
+
// passthrough merely moves it downstream.
|
|
879
|
+
if (options.decompress === false || options.decompress === 'passthrough') return body;
|
|
863
880
|
const encoding = headers.get('content-encoding');
|
|
864
881
|
if (!encoding) return body;
|
|
865
882
|
return decodeBody(body, encoding, options.decoders ?? null, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
866
883
|
}
|
|
867
884
|
|
|
868
|
-
function buildResponse(headInfo, body, framing, conn) {
|
|
885
|
+
function buildResponse(headInfo, body, framing, conn, coded = false) {
|
|
869
886
|
const headers = new Headers();
|
|
870
887
|
for (const [k, v] of headInfo.headers) {
|
|
871
888
|
if (k === 'set-cookie') continue;
|
|
872
889
|
headers.append(k, v);
|
|
873
890
|
}
|
|
874
891
|
for (const c of headInfo.setCookie ?? []) headers.append('set-cookie', c);
|
|
875
|
-
if (headers.has('content-encoding')) {
|
|
892
|
+
if (headers.has('content-encoding') && !coded) {
|
|
876
893
|
// The bytes handed to the caller are decoded, so a byte count describing the encoded form
|
|
877
894
|
// would be a lie. Content-Encoding stays as information about what was on the wire.
|
|
895
|
+
//
|
|
896
|
+
// Under `decompress: 'passthrough'` the bytes are NOT decoded, so the length describes exactly
|
|
897
|
+
// what the caller holds and keeping it is what makes forwarding possible: a proxy that relays
|
|
898
|
+
// this Response needs a Content-Length that agrees with its body.
|
|
878
899
|
headers.delete('content-length');
|
|
879
900
|
}
|
|
880
901
|
|
package/src/tls/connect.js
CHANGED
|
@@ -265,6 +265,7 @@ export async function connectTls({ transport, hostname, verifyPeer, options = {}
|
|
|
265
265
|
const record = new RecordLayer(transport, {
|
|
266
266
|
maxHandshakeMessage: options.maxHandshakeMessage,
|
|
267
267
|
maxKeyUpdates: options.maxKeyUpdates,
|
|
268
|
+
pullBytes: options.pullBytes,
|
|
268
269
|
// ChaCha20-Poly1305 has no WebCrypto path here; its seal/open are injected via deps.aead and
|
|
269
270
|
// threaded to every createAead. Null for the AES-GCM-only default, which needs nothing.
|
|
270
271
|
aeadImpls: deps.aead ?? null,
|
package/src/tls/record.js
CHANGED
|
@@ -118,7 +118,7 @@ export class RecordLayer {
|
|
|
118
118
|
* @param {RecordLayerOptions} [opts]
|
|
119
119
|
*/
|
|
120
120
|
constructor({ readable, writable }, opts = {}) {
|
|
121
|
-
this._r = new ByteReader(readable);
|
|
121
|
+
this._r = new ByteReader(readable, opts.pullBytes);
|
|
122
122
|
this._w = new ByteWriter(writable);
|
|
123
123
|
this._maxHandshakeMessage = opts.maxHandshakeMessage ?? 1 << 17;
|
|
124
124
|
this._maxKeyUpdates = opts.maxKeyUpdates ?? 32;
|
package/src/util/bytes.js
CHANGED
|
@@ -15,6 +15,15 @@ const EMPTY = new Uint8Array(0);
|
|
|
15
15
|
* so a large view never delays delivery; it only lets bytes the transport has already buffered
|
|
16
16
|
* arrive in one crossing instead of many.
|
|
17
17
|
*/
|
|
18
|
+
// Swept on the edge against a real proxied socket, ms of CPU per 4 MB body at the record layer:
|
|
19
|
+
//
|
|
20
|
+
// 16 KiB 42.0 64 KiB 38.5 256 KiB 46.5 1 MiB 57.0
|
|
21
|
+
//
|
|
22
|
+
// A U with its floor on the current value, and going LARGER is worse — 1 MiB costs 48% more than
|
|
23
|
+
// the default, because allocating the view outgrows the boundary crossings it saves. Recorded so
|
|
24
|
+
// the next person to reach for this knob does not have to re-run the sweep to find there is nothing
|
|
25
|
+
// in it: the 42 ms this layer costs on a 4 MB body is the price of moving bytes off a real socket,
|
|
26
|
+
// of which the AEAD is under 2 ms. It is not a tuning problem.
|
|
18
27
|
const BYOB_PULL_BYTES = 65536;
|
|
19
28
|
|
|
20
29
|
/** Raised when the peer stops sending in the middle of a structure we must read whole. */
|
|
@@ -50,8 +59,15 @@ export class UnexpectedEofError extends TunnelFetchError {
|
|
|
50
59
|
* ReadableStream in this package and its tests) take the default-reader path unchanged.
|
|
51
60
|
*/
|
|
52
61
|
export class ByteReader {
|
|
53
|
-
/**
|
|
54
|
-
|
|
62
|
+
/**
|
|
63
|
+
* @param {ReadableStream<Uint8Array>} readable
|
|
64
|
+
* @param {number} [pullBytes] size of each BYOB view pulled from the source. Tunable because it
|
|
65
|
+
* decides how many times a body crosses the runtime boundary on the way in, and that turned out
|
|
66
|
+
* to be the largest single cost in a large response — 42 ms of a 106 ms 4 MB request is socket
|
|
67
|
+
* reads and record decryption, of which the AEAD itself is under 2 ms.
|
|
68
|
+
*/
|
|
69
|
+
constructor(readable, pullBytes = BYOB_PULL_BYTES) {
|
|
70
|
+
this._pullBytes = pullBytes > 0 ? pullBytes : BYOB_PULL_BYTES;
|
|
55
71
|
/** @type {ReadableStreamBYOBReader | null} */
|
|
56
72
|
this._byob = null;
|
|
57
73
|
try {
|
|
@@ -100,7 +116,7 @@ export class ByteReader {
|
|
|
100
116
|
async _pull() {
|
|
101
117
|
if (this._eof) return false;
|
|
102
118
|
const { value, done } = this._byob
|
|
103
|
-
? await this._byob.read(new Uint8Array(
|
|
119
|
+
? await this._byob.read(new Uint8Array(this._pullBytes))
|
|
104
120
|
: await this._reader.read();
|
|
105
121
|
if (done) {
|
|
106
122
|
this._eof = true;
|
|
@@ -398,11 +414,39 @@ export function equal(a, b) {
|
|
|
398
414
|
*/
|
|
399
415
|
export function timingSafeEqual(a, b) {
|
|
400
416
|
if (a.byteLength !== b.byteLength) return false;
|
|
417
|
+
// Prefer the runtime's own, which is compiled rather than interpreted and therefore actually has
|
|
418
|
+
// the property this function is named for. `crypto.subtle.timingSafeEqual` is a non-standard
|
|
419
|
+
// Cloudflare extension, so it is FEATURE-DETECTED rather than assumed — this package runs on
|
|
420
|
+
// Node, Deno and Bun as well, and inferring a capability from a runtime name is the mistake it
|
|
421
|
+
// refuses to make everywhere else. Detected once, at module load, because doing it per call would
|
|
422
|
+
// put a property lookup on a path whose whole point is uniform timing.
|
|
423
|
+
if (NATIVE_TIMING_SAFE_EQUAL) return NATIVE_TIMING_SAFE_EQUAL(a, b);
|
|
401
424
|
let diff = 0;
|
|
402
425
|
for (let i = 0; i < a.byteLength; i++) diff |= a[i] ^ b[i];
|
|
403
426
|
return diff === 0;
|
|
404
427
|
}
|
|
405
428
|
|
|
429
|
+
/**
|
|
430
|
+
* The runtime's constant-time compare, bound once, or null where there is none.
|
|
431
|
+
*
|
|
432
|
+
* Bound with a probe rather than a typeof check: a property that exists but throws on real input
|
|
433
|
+
* would otherwise be discovered inside a TLS Finished verification, where the failure mode is a
|
|
434
|
+
* dead connection on a path that is supposed to be the careful one.
|
|
435
|
+
*/
|
|
436
|
+
const NATIVE_TIMING_SAFE_EQUAL = (() => {
|
|
437
|
+
const fn = globalThis.crypto?.subtle?.timingSafeEqual;
|
|
438
|
+
if (typeof fn !== 'function') return null;
|
|
439
|
+
try {
|
|
440
|
+
const one = Uint8Array.of(1, 2, 3);
|
|
441
|
+
const two = Uint8Array.of(1, 2, 4);
|
|
442
|
+
if (fn.call(globalThis.crypto.subtle, one, one) !== true) return null;
|
|
443
|
+
if (fn.call(globalThis.crypto.subtle, one, two) !== false) return null;
|
|
444
|
+
} catch {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
return (a, b) => fn.call(globalThis.crypto.subtle, a, b);
|
|
448
|
+
})();
|
|
449
|
+
|
|
406
450
|
const HEX = '0123456789abcdef';
|
|
407
451
|
/**
|
|
408
452
|
* @param {Uint8Array} bytes
|
package/types/client.d.ts
CHANGED
|
@@ -58,7 +58,11 @@ export function install(options?: ClientOptions): () => void;
|
|
|
58
58
|
* again on the DECODED output — a compressed body within the cap can decompress far past it, and
|
|
59
59
|
* a registered decoder's output is bounded by it too. Pass `Infinity` to opt out, which is the
|
|
60
60
|
* right choice for streaming large files and the wrong one for fetching URLs you do not control.
|
|
61
|
-
* @property {boolean} [decompress] gzip/deflate. Default true.
|
|
61
|
+
* @property {boolean | 'passthrough'} [decompress] gzip/deflate. Default true. `false` neither
|
|
62
|
+
* advertises nor decodes a coding, so the origin sends plaintext and the wire grows.
|
|
63
|
+
* `'passthrough'` advertises gzip as usual but hands the CODED body back with its
|
|
64
|
+
* `Content-Encoding` intact — 30% cheaper on a 4 MB body and 2.76x fewer wire bytes, and correct
|
|
65
|
+
* only for a caller that forwards the body rather than reading it.
|
|
62
66
|
* @property {Record<string, import('./client/decode.js').BodyDecoder>} [decoders] extra
|
|
63
67
|
* content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
|
|
64
68
|
* makes advertising it honest, so each name is appended to Accept-Encoding — a client that
|
|
@@ -239,9 +243,13 @@ export type ClientOptions = {
|
|
|
239
243
|
*/
|
|
240
244
|
maxBodyBytes?: number | undefined;
|
|
241
245
|
/**
|
|
242
|
-
* gzip/deflate. Default true.
|
|
246
|
+
* gzip/deflate. Default true. `false` neither
|
|
247
|
+
* advertises nor decodes a coding, so the origin sends plaintext and the wire grows.
|
|
248
|
+
* `'passthrough'` advertises gzip as usual but hands the CODED body back with its
|
|
249
|
+
* `Content-Encoding` intact — 30% cheaper on a 4 MB body and 2.76x fewer wire bytes, and correct
|
|
250
|
+
* only for a caller that forwards the body rather than reading it.
|
|
243
251
|
*/
|
|
244
|
-
decompress?: boolean | undefined;
|
|
252
|
+
decompress?: boolean | "passthrough" | undefined;
|
|
245
253
|
/**
|
|
246
254
|
* extra
|
|
247
255
|
* content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
|
package/types/util/bytes.d.ts
CHANGED
|
@@ -83,8 +83,15 @@ export class UnexpectedEofError extends TunnelFetchError {
|
|
|
83
83
|
* ReadableStream in this package and its tests) take the default-reader path unchanged.
|
|
84
84
|
*/
|
|
85
85
|
export class ByteReader {
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @param {ReadableStream<Uint8Array>} readable
|
|
88
|
+
* @param {number} [pullBytes] size of each BYOB view pulled from the source. Tunable because it
|
|
89
|
+
* decides how many times a body crosses the runtime boundary on the way in, and that turned out
|
|
90
|
+
* to be the largest single cost in a large response — 42 ms of a 106 ms 4 MB request is socket
|
|
91
|
+
* reads and record decryption, of which the AEAD itself is under 2 ms.
|
|
92
|
+
*/
|
|
93
|
+
constructor(readable: ReadableStream<Uint8Array>, pullBytes?: number);
|
|
94
|
+
_pullBytes: number;
|
|
88
95
|
/** @type {ReadableStreamBYOBReader | null} */
|
|
89
96
|
_byob: ReadableStreamBYOBReader | null;
|
|
90
97
|
_reader: ReadableStreamBYOBReader | ReadableStreamDefaultReader<Uint8Array<ArrayBufferLike>> | null;
|