tunnelfetch 1.0.1 → 1.1.1

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/types/client.d.ts CHANGED
@@ -54,8 +54,16 @@ export function install(options?: ClientOptions): () => void;
54
54
  * one across Clients or to persist it.
55
55
  * @property {number} [maxRedirects] default 20.
56
56
  * @property {number} [maxBodyBytes] enforced from Content-Length before a byte is read.
57
- * @property {boolean} [decompress] gzip/deflate. Default true. Never `br`; the runtime cannot
58
- * decompress it, so it is never advertised either.
57
+ * @property {boolean} [decompress] gzip/deflate. Default true.
58
+ * @property {Record<string, import('./client/decode.js').BodyDecoder>} [decoders] extra
59
+ * content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
60
+ * makes advertising it honest, so each name is appended to Accept-Encoding — a client that
61
+ * asked for a coding it cannot decode would turn every such response into garbage. `br` and
62
+ * `zstd` are not built in because the runtime's DecompressionStream has neither and this
63
+ * package takes no dependencies; supply your own and the cost, and the supply chain, are
64
+ * yours and visible. Measured on the edge: WASM brotli decodes at about 2x native gzip, and
65
+ * the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
66
+ * matching a browser's Accept-Encoding, not saving CPU.
59
67
  * @property {boolean} [keepAlive] default true.
60
68
  * @property {boolean} [http2] offer HTTP/2 via ALPN and speak it when the server selects it.
61
69
  * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
@@ -75,95 +83,7 @@ export class Client {
75
83
  * @param {ClientOptions} [options]
76
84
  */
77
85
  constructor(options?: ClientOptions);
78
- options: {
79
- /**
80
- * Socket factory. Required for any
81
- * request the platform's own `fetch` cannot serve — which is every proxied request, and every
82
- * request asking for a trust policy `fetch` cannot express.
83
- */
84
- connect?: import("./proxy/index.js").ConnectFn | undefined;
85
- /**
86
- * URL string or
87
- * config object; `http:`, `https:`, `socks5:` and `socks5h:`.
88
- */
89
- proxy?: string | import("./proxy/index.js").ProxyConfig | null | undefined;
90
- /**
91
- * certificate policy, httpx's
92
- * `verify=`. Default `{ mode: 'system' }`.
93
- */
94
- trust?: import("./trust/index.js").TrustConfig | undefined;
95
- /**
96
- * handshake knobs.
97
- */
98
- tls?: import("./tls/connect.js").TlsOptions | undefined;
99
- /**
100
- * connect / handshake /
101
- * headers / idle / total, in ms. The idle gap is the control; total is a backstop.
102
- */
103
- timeouts?: import("./util/deadline.js").DeadlineOptions | undefined;
104
- /**
105
- * enable a per-Client cookie jar.
106
- */
107
- cookies?: boolean | undefined;
108
- /**
109
- * supply a jar directly, e.g. to share
110
- * one across Clients or to persist it.
111
- */
112
- jar?: CookieJar | undefined;
113
- /**
114
- * default 20.
115
- */
116
- maxRedirects?: number | undefined;
117
- /**
118
- * enforced from Content-Length before a byte is read.
119
- */
120
- maxBodyBytes?: number | undefined;
121
- /**
122
- * gzip/deflate. Default true. Never `br`; the runtime cannot
123
- * decompress it, so it is never advertised either.
124
- */
125
- decompress?: boolean | undefined;
126
- /**
127
- * default true.
128
- */
129
- keepAlive?: boolean | undefined;
130
- /**
131
- * offer HTTP/2 via ALPN and speak it when the server selects it.
132
- * Default true. The goal is ACCESS, not speed — some sites treat HTTP/1.1 as a bot signal — and
133
- * on a CPU-billed runtime h2 costs MORE than h1 (HPACK is extra work). Set false to offer only
134
- * `http/1.1`. There is no fallback-and-retry either way: the server's ALPN pick is followed.
135
- */
136
- http2?: boolean | undefined;
137
- /**
138
- * never delegate to the platform's fetch, even when it could
139
- * serve the request. Mainly for exercising this stack against origins that do not need it.
140
- */
141
- forceTunnel?: boolean | undefined;
142
- /**
143
- * delegation target; defaults to `globalThis.fetch`.
144
- */
145
- nativeFetch?: FetchLike | undefined;
146
- /**
147
- * connection pool sizing.
148
- */
149
- pool?: {
150
- maxPerKey?: number;
151
- maxTotal?: number;
152
- } | undefined;
153
- limits?: Limits | undefined;
154
- /**
155
- * injectable randomness and key generation.
156
- */
157
- deps?: import("./tls/connect.js").TlsDeps | undefined;
158
- /**
159
- * aborts every request this Client makes.
160
- */
161
- signal?: AbortSignal | undefined;
162
- /**
163
- * epoch ms override, for certificate validity in tests.
164
- */
165
- now?: number | undefined;
166
- };
86
+ options: Readonly<ClientOptions>;
167
87
  pool: ConnectionPool;
168
88
  /** @type {Map<string, import('./http2/connection.js').Http2Connection>} */
169
89
  _h2: Map<string, import("./http2/connection.js").Http2Connection>;
@@ -172,6 +92,8 @@ export class Client {
172
92
  jar: CookieJar | null;
173
93
  tickets: TicketStore;
174
94
  _closed: boolean;
95
+ /** @type {Set<Promise<void>>} */
96
+ _inflight: Set<Promise<void>>;
175
97
  /**
176
98
  * @param {RequestInfo | URL} input
177
99
  * @param {RequestInit} [init]
@@ -180,6 +102,16 @@ export class Client {
180
102
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response & {
181
103
  tunnelfetch?: ResponseDetail;
182
104
  }>;
105
+ /**
106
+ * Resolve once every response body handed out by this Client has finished, one way or another —
107
+ * read to the end, cancelled, or failed. Deliberately NOT folded into close(): close() is the
108
+ * forceful teardown, and a teardown that waits on the streams it is tearing down would hang on
109
+ * any body the caller abandoned. This is for callers that want the graceful order.
110
+ *
111
+ * Looping rather than a single Promise.all because a body settling can start another (a redirect
112
+ * drains its predecessor), and a set sampled once would miss the successor.
113
+ */
114
+ idle(): Promise<void>;
183
115
  /** Release every pooled socket and shared HTTP/2 connection. A Client that is finished must be
184
116
  * closed or sockets leak for the isolate's lifetime. */
185
117
  close(): Promise<void>;
@@ -271,10 +203,21 @@ export type ClientOptions = {
271
203
  */
272
204
  maxBodyBytes?: number | undefined;
273
205
  /**
274
- * gzip/deflate. Default true. Never `br`; the runtime cannot
275
- * decompress it, so it is never advertised either.
206
+ * gzip/deflate. Default true.
276
207
  */
277
208
  decompress?: boolean | undefined;
209
+ /**
210
+ * extra
211
+ * content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
212
+ * makes advertising it honest, so each name is appended to Accept-Encoding — a client that
213
+ * asked for a coding it cannot decode would turn every such response into garbage. `br` and
214
+ * `zstd` are not built in because the runtime's DecompressionStream has neither and this
215
+ * package takes no dependencies; supply your own and the cost, and the supply chain, are
216
+ * yours and visible. Measured on the edge: WASM brotli decodes at about 2x native gzip, and
217
+ * the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
218
+ * matching a browser's Accept-Encoding, not saving CPU.
219
+ */
220
+ decoders?: Record<string, import("./client/decode.js").BodyDecoder> | undefined;
278
221
  /**
279
222
  * default true.
280
223
  */
@@ -316,8 +259,8 @@ export type ClientOptions = {
316
259
  */
317
260
  now?: number | undefined;
318
261
  };
319
- import { CookieJar } from './client/cookies.js';
320
262
  import { ConnectionPool } from './pool.js';
263
+ import { CookieJar } from './client/cookies.js';
321
264
  import { TicketStore } from './tls/tickets.js';
322
265
  import { utf8 } from './util/bytes.js';
323
266
  export { CookieJar, ConnectionPool, utf8 };
@@ -34,19 +34,6 @@ export function buildRequestFields({ method, scheme, authority, path, headers }:
34
34
  */
35
35
  export class Http2Retryable extends Http2Error {
36
36
  }
37
- /**
38
- * @typedef {object} Http2ConnectionOptions
39
- * @property {import('../transport.js').ConnectionInfo} [info] provenance attached to responses
40
- * @property {number} [initialWindowSize] our SETTINGS_INITIAL_WINDOW_SIZE (receive window per
41
- * stream). Defaults to curl's 10 MiB; tests lower it to exercise flow control.
42
- * @property {number} [connectionWindow] the connection receive window we open with a WINDOW_UPDATE
43
- * right after SETTINGS. Defaults to curl's 1000 MiB.
44
- * @property {number} [maxConcurrentStreams] our advertised SETTINGS_MAX_CONCURRENT_STREAMS.
45
- * @property {number} [maxHeaderTableSize] our advertised SETTINGS_HEADER_TABLE_SIZE.
46
- * @property {number} [maxHeaderListSize] self-protection cap on a decoded response header list.
47
- * @property {(err: Error | null) => void} [onClose] called once when the connection dies, so a
48
- * registry can drop it.
49
- */
50
37
  export class Http2Connection {
51
38
  /**
52
39
  * @param {import('../tls/connect.js').ByteDuplex | { readable: ReadableStream<Uint8Array>,
@@ -83,8 +70,10 @@ export class Http2Connection {
83
70
  _continuation: {
84
71
  streamId: any;
85
72
  fragments: Uint8Array<ArrayBuffer>[];
73
+ bytes: number;
86
74
  endStream: boolean;
87
75
  } | null;
76
+ _maxHeaderBlockBytes: number;
88
77
  _expectFirstSettings: boolean;
89
78
  _fatal: any;
90
79
  _goaway: {
@@ -137,6 +126,9 @@ export class Http2Connection {
137
126
  /** @type {Uint8Array[]} */
138
127
  recvQueue: Uint8Array[];
139
128
  recvEnded: boolean;
129
+ /** @type {number | null} declared content-length, null when absent */
130
+ declaredLength: number | null;
131
+ receivedLength: number;
140
132
  /** @type {Error | null} */
141
133
  bodyError: Error | null;
142
134
  /** @type {(() => void) | null} */
@@ -187,6 +179,14 @@ export class Http2Connection {
187
179
  _dispatchFrame(frame: any): void;
188
180
  _onSettings(flags: any, streamId: any, payload: any): void;
189
181
  _onHeaders(flags: any, streamId: any, payload: any): void;
182
+ /**
183
+ * Enforce the raw header-block cap, killing the connection when it is passed.
184
+ * Connection-level rather than stream-level on purpose: the fragments are HPACK input, and
185
+ * abandoning a partial block would leave the shared decoder desynchronised for every other
186
+ * stream — which RFC 9113 s4.3 makes a connection error in its own right.
187
+ * @returns {boolean} true when assembly may continue
188
+ */
189
+ _headerBlockWithinCap(): boolean;
190
190
  _onContinuation(flags: any, payload: any): void;
191
191
  /** A full header block has been assembled: HPACK-decode it (connection-fatal on failure, since
192
192
  * HPACK state is shared) and route it to the stream as a response head or as trailers. */
@@ -225,24 +225,6 @@ export class Http2Connection {
225
225
  _settleResolve(d: any, value: any): void;
226
226
  _settleReject(d: any, err: any): void;
227
227
  }
228
- export type BodyStream = ReadableStream<Uint8Array> & {
229
- completed: Promise<boolean>;
230
- trailers: Promise<Headers | null>;
231
- };
232
- export type Http2ResponseHead = {
233
- status: number;
234
- /**
235
- * always '' — HTTP/2 has no reason phrase
236
- */
237
- statusText: string;
238
- headers: Headers;
239
- /**
240
- * one entry per set-cookie field, kept separate like the h1 path
241
- */
242
- setCookie: string[];
243
- httpVersion: "2";
244
- body: BodyStream;
245
- };
246
228
  export type Http2ConnectionOptions = {
247
229
  /**
248
230
  * provenance attached to responses
@@ -270,12 +252,37 @@ export type Http2ConnectionOptions = {
270
252
  * self-protection cap on a decoded response header list.
271
253
  */
272
254
  maxHeaderListSize?: number | undefined;
255
+ /**
256
+ * cap on the RAW bytes of one HEADERS+CONTINUATION run,
257
+ * before HPACK decoding. Default 262144, matching the decoded cap. This is the bound that stops
258
+ * a CONTINUATION flood; `maxHeaderListSize` cannot, because it is only reachable once the whole
259
+ * block has been assembled in memory.
260
+ */
261
+ maxHeaderBlockBytes?: number | undefined;
273
262
  /**
274
263
  * called once when the connection dies, so a
275
264
  * registry can drop it.
276
265
  */
277
266
  onClose?: ((err: Error | null) => void) | undefined;
278
267
  };
268
+ export type BodyStream = ReadableStream<Uint8Array> & {
269
+ completed: Promise<boolean>;
270
+ trailers: Promise<Headers | null>;
271
+ };
272
+ export type Http2ResponseHead = {
273
+ status: number;
274
+ /**
275
+ * always '' — HTTP/2 has no reason phrase
276
+ */
277
+ statusText: string;
278
+ headers: Headers;
279
+ /**
280
+ * one entry per set-cookie field, kept separate like the h1 path
281
+ */
282
+ setCookie: string[];
283
+ httpVersion: "2";
284
+ body: BodyStream;
285
+ };
279
286
  import { Http2Error } from '../errors.js';
280
287
  import { ByteReader } from '../util/bytes.js';
281
288
  import { ByteWriter } from '../util/bytes.js';