quico 0.3.0 → 0.7.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.
@@ -0,0 +1,675 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import net from 'node:net';
16
+ import { EventEmitter } from 'node:events';
17
+
18
+ import { H2Connection, SOCKET_HIGH_WATER } from './h2.js';
19
+ import * as F from './h2_frames.js';
20
+ import { buildRequestPseudoHeaders, corkForTick, trailerFields, formatPriority } from './http_common.js';
21
+ import { IncomingMessage } from './streams.js';
22
+ import { tlsIdentity } from './agent.js';
23
+
24
+
25
+ // ============================================================
26
+ // HTTP/2 client.
27
+ //
28
+ // The pool here is the OPPOSITE shape to h1_client's, and the difference is
29
+ // the whole point of the protocol. HTTP/1.1 carries one request per socket,
30
+ // so its pool maps an origin to a LIST of idle sockets and concurrency means
31
+ // more sockets. HTTP/2 multiplexes, so this pool maps an origin to ONE
32
+ // session and concurrency means more streams on it.
33
+ //
34
+ // agent.js already keeps _h1Pool, _h2Pool and _h3Pool as separate maps for
35
+ // exactly this reason. Forcing the two shapes into one abstraction would
36
+ // distort both — and the H3 pool, which is the same shape as this one, is
37
+ // where the coalescing logic worth copying already lives.
38
+ // ============================================================
39
+
40
+
41
+ function H2Pool(options) {
42
+ options = options || {};
43
+ var sessions = {}; // "host:port" -> session
44
+ var pending = {}; // "host:port" -> [callbacks] while one is being established
45
+
46
+ function key(host, port) { return host + ':' + port; }
47
+
48
+ return {
49
+ get: function (host, port) {
50
+ var s = sessions[key(host, port)];
51
+ if (!s) return null;
52
+ if (s.destroyed || s.goingAway) { delete sessions[key(host, port)]; return null; }
53
+ return s;
54
+ },
55
+
56
+ set: function (host, port, session) {
57
+ var k = key(host, port);
58
+ sessions[k] = session;
59
+ session.on('close', function () { if (sessions[k] === session) delete sessions[k]; });
60
+ },
61
+
62
+ /**
63
+ * Coalesce concurrent connection attempts to one origin.
64
+ *
65
+ * Without this, N requests issued in the same tick each miss the pool and
66
+ * open N sessions to the same host — N handshakes, N HPACK table pairs,
67
+ * zero multiplexing. It is the exact failure the H3 agent already guards
68
+ * against, and it is invisible in any test that issues requests one at a
69
+ * time.
70
+ *
71
+ * Returns false to the first caller, which becomes the lead and MUST call
72
+ * settle(); true to every later one, whose callback fires when the lead
73
+ * finishes and which then simply retries its pool lookup.
74
+ */
75
+ join: function (host, port, cb) {
76
+ var k = key(host, port);
77
+ if (pending[k]) { pending[k].push(cb); return true; }
78
+ pending[k] = [];
79
+ return false;
80
+ },
81
+
82
+ settle: function (host, port, session) {
83
+ var k = key(host, port);
84
+ var waiters = pending[k] || [];
85
+ delete pending[k];
86
+ if (session) this.set(host, port, session);
87
+ for (var i = 0; i < waiters.length; i++) {
88
+ try { waiters[i](); } catch (e) { /* a waiter's failure is its own */ }
89
+ }
90
+ },
91
+
92
+ destroy: function () {
93
+ for (var k in sessions) { try { sessions[k].destroy(); } catch (e) {} }
94
+ sessions = {};
95
+ pending = {};
96
+ }
97
+ };
98
+ }
99
+
100
+ var globalPool = new H2Pool();
101
+
102
+
103
+ // ============================================================
104
+ // Session — one connection, many streams
105
+ // ============================================================
106
+
107
+ function H2Session(socket, options) {
108
+ var ev = new EventEmitter();
109
+ var h2 = new H2Connection({
110
+ isServer: false,
111
+ // The stream window a response may fill before we return credit: 6 MB,
112
+ // Chrome's. HTTP/2's own default, 64 KB, holds one response to 64 KB a
113
+ // round trip — 5 Mbps over 100 ms — and brings a WINDOW_UPDATE every
114
+ // 32 KB. Credit still goes back only as the application reads
115
+ // (autoWindowUpdate false), so a response nobody reads stops at 6 MB.
116
+ // The server's side keeps HTTP/2's default (h2_server): what it
117
+ // advertises bounds every client's uploads.
118
+ initialWindowSize: options.initialWindowSize != null ? options.initialWindowSize : 6 * 1024 * 1024,
119
+ maxHeaderListSize: options.maxHeaderListSize,
120
+ autoWindowUpdate: false,
121
+ // DATA is held while the socket's unsent bytes are over this mark, as on
122
+ // the server (h2_server): the flow-control windows bound what the SERVER
123
+ // may receive, this what we queue for a socket that is not draining.
124
+ // Without it every request body went straight into the socket, whatever
125
+ // the network took — an upload to a slow server sat in memory whole.
126
+ isWriteBlocked: function () { return socket.writableLength >= SOCKET_HIGH_WATER; }
127
+ });
128
+ // node:http2's maxSessionMemory (MB, default 10): what may be queued on the
129
+ // socket. DATA waits (above); what can pile up is everything else — the
130
+ // replies a server provokes (PING and SETTINGS acknowledgements) while it
131
+ // does not read. Past the bound the session goes.
132
+ var maxSessionMemory = (options.maxSessionMemory > 0 ? options.maxSessionMemory : 10) * 1024 * 1024;
133
+
134
+ var streams = {};
135
+ var destroyed = false;
136
+ var goingAway = false;
137
+
138
+ h2.on('send', function (data) {
139
+ if (destroyed || !socket.writable) return;
140
+ corkForTick(socket);
141
+ try { socket.write(data); } catch (e) { fail(e); return; }
142
+ if (socket.writableLength > maxSessionMemory) {
143
+ var e = new Error('server is not reading: ' + socket.writableLength + ' bytes queued');
144
+ e.code = 'ERR_HTTP2_SESSION_MEMORY';
145
+ fail(e);
146
+ }
147
+ });
148
+ // DATA was waiting for the socket; it has room again.
149
+ socket.on('drain', function () { if (!destroyed) h2.flushAll(); });
150
+
151
+ h2.on('error', function (err) { fail(err); });
152
+
153
+ h2.on('goaway', function (lastStreamId, code) {
154
+ goingAway = true;
155
+ session.goingAway = true;
156
+ // Streams above lastStreamId were never processed, so they are safe to
157
+ // retry on a fresh connection — that is what the id is FOR.
158
+ for (var id in streams) {
159
+ if (Number(id) > lastStreamId) {
160
+ var e = new Error('stream not processed before GOAWAY');
161
+ e.code = 'ERR_HTTP2_GOAWAY_UNPROCESSED';
162
+ e.retryable = true;
163
+ rejectStream(Number(id), e);
164
+ }
165
+ }
166
+ ev.emit('goaway', lastStreamId, code);
167
+ });
168
+
169
+ h2.on('headers', function (streamId, headers) {
170
+ var st = streams[streamId];
171
+ if (!st) return;
172
+
173
+ var status = parseInt(headers[':status'], 10);
174
+
175
+ // 1xx other than 101 is informational: the real response follows on the
176
+ // same stream, so it must not be delivered as THE response.
177
+ if (status >= 100 && status < 200) {
178
+ ev.emit('information', streamId, headers);
179
+ // And on the request, as node:http's 'information' (103 Early Hints).
180
+ if (st.onInformation) {
181
+ var ih = {}, raw = [];
182
+ for (var hn in headers) { if (hn[0] !== ':') { ih[hn] = headers[hn]; raw.push(hn, String(headers[hn])); } }
183
+ st.onInformation({ statusCode: status, statusMessage: '', httpVersion: '2.0', httpVersionMajor: 2, httpVersionMinor: 0, headers: ih, rawHeaders: raw });
184
+ }
185
+ return;
186
+ }
187
+
188
+ var flat = {};
189
+ for (var name in headers) { if (name[0] !== ':') flat[name] = headers[name]; }
190
+
191
+ var res = new IncomingMessage({
192
+ httpVersion: '2.0',
193
+ statusCode: status,
194
+ statusMessage: '', // HTTP/2 has no reason phrase
195
+ headers: flat,
196
+ streamId: streamId,
197
+ socket: socket
198
+ });
199
+ res.on('data', function (chunk) { h2.consume(streamId, chunk.length); });
200
+
201
+ st.res = res;
202
+ st.onResponse(res);
203
+ });
204
+
205
+ h2.on('data', function (streamId, chunk) {
206
+ var st = streams[streamId];
207
+ if (st && st.res) st.res._pushData(chunk);
208
+ });
209
+
210
+ h2.on('trailers', function (streamId, trailers) {
211
+ var st = streams[streamId];
212
+ if (st && st.res) st.res.trailers = trailers;
213
+ });
214
+
215
+ h2.on('end', function (streamId) {
216
+ var st = streams[streamId];
217
+ if (!st) return;
218
+ if (st.res) st.res._endData();
219
+ delete streams[streamId];
220
+ });
221
+
222
+ h2.on('rst', function (streamId, code) {
223
+ var e = new Error('stream reset: ' + (F.ERROR_NAME[code] || code));
224
+ e.code = 'ERR_HTTP2_STREAM_ERROR';
225
+ // REFUSED_STREAM specifically means the server never processed it.
226
+ e.retryable = (code === F.ERROR.REFUSED_STREAM);
227
+ // Any other code: the server had the request and ended it — it is the
228
+ // answer, not a failure to reach the server. Not sent again over HTTP/1.1
229
+ // (a POST would have run twice) under "all protocols failed".
230
+ if (!e.retryable) e.requestError = true;
231
+ rejectStream(streamId, e);
232
+ });
233
+
234
+ socket.on('data', function (chunk) {
235
+ if (destroyed) return;
236
+ try { h2.execute(chunk); } catch (err) { fail(err); }
237
+ });
238
+ socket.on('error', fail);
239
+ socket.on('close', function () { fail(new Error('socket closed')); });
240
+
241
+ // The connection lost under a request already sent, before its response:
242
+ // the server may have acted on it. An idempotent one may go again over
243
+ // another protocol (RFC 9110 §9.2.2); any other is not sent twice — the
244
+ // error is the request's (requestError: the unified client tries no other
245
+ // protocol). A POST lost so was sent again over HTTP/1.1.
246
+ var IDEMPOTENT = { GET: 1, HEAD: 1, OPTIONS: 1, TRACE: 1, PUT: 1, DELETE: 1 };
247
+ function sentAndLost(st, err) {
248
+ if (!st.sent || (err && (err.retryable || err.requestError)) || IDEMPOTENT[st.method]) return err;
249
+ var e = new Error('the connection was lost after the request was sent: ' + (err && err.message), { cause: err });
250
+ e.code = (err && err.code) || 'ECONNRESET';
251
+ e.requestError = true;
252
+ return e;
253
+ }
254
+
255
+ function rejectStream(streamId, err) {
256
+ var st = streams[streamId];
257
+ if (!st) return;
258
+ delete streams[streamId];
259
+ if (st.res) st.res._abort(err);
260
+ else st.onError(err);
261
+ }
262
+
263
+ function fail(err) {
264
+ if (destroyed) return;
265
+ destroyed = true;
266
+ session.destroyed = true;
267
+ for (var id in streams) {
268
+ var st = streams[id];
269
+ if (st.res) st.res._abort(err);
270
+ else st.onError(sentAndLost(st, err));
271
+ }
272
+ streams = {};
273
+ try { socket.destroy(); } catch (e) {}
274
+ ev.emit('close');
275
+ }
276
+
277
+ h2.sendPreface();
278
+
279
+ var session = {
280
+ destroyed: false,
281
+ goingAway: false,
282
+ on: function (n, f) { ev.on(n, f); return session; },
283
+
284
+ request: function (headerList, callbacks) {
285
+ if (destroyed) { callbacks.onError(new Error('session destroyed')); return null; }
286
+ if (goingAway) {
287
+ var e = new Error('session is going away');
288
+ e.retryable = true;
289
+ callbacks.onError(e);
290
+ return null;
291
+ }
292
+ var streamId = h2.nextStreamId();
293
+ streams[streamId] = { onResponse: callbacks.onResponse, onError: callbacks.onError, onInformation: callbacks.onInformation, res: null, method: callbacks.method || null, sent: false };
294
+ return streamId;
295
+ },
296
+
297
+ sendHeaders: function (streamId, list, endStream) { if (streams[streamId]) streams[streamId].sent = true; h2.sendHeaders(streamId, list, endStream); },
298
+ sendData: function (streamId, data, endStream) { return h2.sendData(streamId, data, endStream); },
299
+ // A request's trailers (node:http's addTrailers): HEADERS with END_STREAM.
300
+ sendTrailers: function (streamId, fields) { h2.sendTrailers(streamId, fields); },
301
+ // A request's new priority, after it went (RFC 9218 §7.1).
302
+ sendPriorityUpdate: function (streamId, p) { h2.sendPriorityUpdate(streamId, p); },
303
+ // Only a stream still open: RST_STREAM on one already closed is not ours
304
+ // to send. Forgotten before the RST goes: h2 reports a reset it sends as
305
+ // it does one it receives ('rst'), and a cancellation of our own read as
306
+ // the server's reset — the response destroyed with an error nobody was
307
+ // listening for.
308
+ cancel: function (streamId) {
309
+ if (!(streamId in streams)) return;
310
+ delete streams[streamId];
311
+ h2.sendRstStream(streamId, F.ERROR.CANCEL);
312
+ },
313
+ onDrain: function (fn) { h2.on('drain', fn); },
314
+ offDrain: function (fn) { h2.off('drain', fn); },
315
+ ping: function (payload) { h2.sendPing(payload); },
316
+ destroy: function () { fail(new Error('destroyed')); },
317
+
318
+ get activeStreams() { return Object.keys(streams).length; }
319
+ };
320
+
321
+ return session;
322
+ }
323
+
324
+
325
+ // ============================================================
326
+ // request()
327
+ // ============================================================
328
+
329
+ /**
330
+ * request(options, callback) -> an EventEmitter that emits 'response' with an
331
+ * IncomingMessage. The same shape as h1_client's, so the unified client can
332
+ * treat the two interchangeably.
333
+ */
334
+ function request(options, callback) {
335
+ options = options || {};
336
+
337
+ var hostname = options.hostname || options.host || 'localhost';
338
+ var port = parseInt(options.port, 10) || (options.tls ? 443 : 80);
339
+ var method = String(options.method || 'GET').toUpperCase();
340
+ var pool = options.pool || globalPool;
341
+ // The pool's name for this origin: the host, and over TLS what decides the
342
+ // connection's identity (agent.js tlsIdentity) — one opened without
343
+ // verification, or with another client certificate, carries no other
344
+ // request. The connection itself goes to `hostname`.
345
+ var poolHost = options.tls ? hostname + '#' + tlsIdentity(options) : hostname;
346
+
347
+ var ev = new EventEmitter();
348
+ // The body. Written before the stream exists (the connection is still
349
+ // being made), it waits here; once the stream exists, each write goes out
350
+ // as it comes (streaming), and write() answers as a stream's does — false
351
+ // while the send window or the socket is full, then 'drain'. It used to
352
+ // keep the whole body until end() and answer true to every write: an
353
+ // upload was held in memory entire, with nothing telling the writer to
354
+ // slow down.
355
+ var bodyChunks = [];
356
+ var bodyBuffered = 0;
357
+ var needDrain = false;
358
+ var streaming = false; // HEADERS sent without END_STREAM; DATA follows as written
359
+ var drainHooked = false;
360
+ var BODY_HIGH_WATER = 64 * 1024; // what write() takes before the stream exists, before saying wait
361
+ var ended = false;
362
+ var settled = false;
363
+ var session = null;
364
+ var streamId = null;
365
+ var sent = false;
366
+ // 0-RTT (TLS early data, RFC 8446 §4.2.10): with earlyData and a session
367
+ // whose ticket allows it, a GET, HEAD or OPTIONS goes in the first flight
368
+ // — the preface, SETTINGS and HEADERS with the ClientHello. Not taken, the
369
+ // TLS layer sends them again after the handshake; answered 425 (RFC 8470),
370
+ // the request fails retryable, and the unified client sends it again on
371
+ // the connection, now up. this.earlyData: 'accepted', 'rejected' or null.
372
+ // Decided when a connection is made (connect): the session is read there,
373
+ // so a request on a pooled session uses none up (the unified client's is
374
+ // taken from its agent on that read).
375
+ var wantEarly = false;
376
+ var sentEarly = false;
377
+ ev.earlyData = null;
378
+ var reqTrailers = null;
379
+
380
+ function fail(err) {
381
+ if (settled) return;
382
+ settled = true;
383
+ ev.emit('error', err);
384
+ }
385
+
386
+ function connect() {
387
+ var existing = pool.get(poolHost, port);
388
+ if (existing) { start(existing); return; }
389
+
390
+ if (pool.join(poolHost, port, function () {
391
+ // The lead settled; retry from the top. On success the pool lookup now
392
+ // hits and we multiplex on its session; on failure we become the next
393
+ // lead — a serial retry, never a thundering herd.
394
+ if (!settled) connect();
395
+ })) return;
396
+
397
+ var socket, earlySession = null;
398
+ var sessionNow = options.session;
399
+ wantEarly = options.earlyData === true && !!sessionNow && !!options.tls && (method === 'GET' || method === 'HEAD' || method === 'OPTIONS');
400
+ var onReady = function () {
401
+ var s = new H2Session(socket, options);
402
+ pool.settle(poolHost, port, s);
403
+ start(s);
404
+ };
405
+
406
+ try {
407
+ if (options.tls) {
408
+ socket = options.tls.connect({
409
+ host: hostname,
410
+ port: port,
411
+ servername: options.servername || hostname,
412
+ // ALPN is not optional for h2 over TLS (RFC 9113 §3.2): a server
413
+ // that does not select 'h2' is not speaking it, and sending the
414
+ // preface anyway would be talking past it.
415
+ ALPNProtocols: ['h2'],
416
+ rejectUnauthorized: options.rejectUnauthorized !== false,
417
+ ca: options.ca,
418
+ // A client certificate, for a server that asks (tls.connect's names).
419
+ key: options.key, cert: options.cert, passphrase: options.passphrase,
420
+ // A saved session to resume with (tls.connect's `session`).
421
+ session: sessionNow,
422
+ // The server's certificate chain may come compressed (RFC 8879):
423
+ // brotli, zstd, zlib — as the HTTP/3 client and the browsers offer.
424
+ certificateCompression: true,
425
+ // The server's stapled OCSP response ('OCSPResponse').
426
+ requestOCSP: options.requestOCSP === true,
427
+ // Encrypted Client Hello: the server's ECHConfigList, or GREASE ECH.
428
+ ech: options.ech || undefined,
429
+ echGrease: options.echGrease === true,
430
+ // The key-exchange groups to offer (tls.connect's ecdhCurve).
431
+ ecdhCurve: options.ecdhCurve,
432
+ sigalgs: options.sigalgs,
433
+ earlyData: wantEarly,
434
+ // TCP_NODELAY, as node:http's client sets it (its noDelay, default true).
435
+ // Without it Nagle held a small write — a request's HEADERS behind the
436
+ // preface, a WINDOW_UPDATE, a body after its head — until the last one
437
+ // was acknowledged, and the server's delayed ACK made that up to 40 ms:
438
+ // 3000 small HTTP/2 requests took 2.8 s instead of 0.5.
439
+ noDelay: options.noDelay !== false
440
+ }, function () {
441
+ if (socket.alpnProtocol !== 'h2') {
442
+ pool.settle(poolHost, port, null);
443
+ fail(new Error('server did not negotiate h2 (got ' +
444
+ JSON.stringify(socket.alpnProtocol) + ')'));
445
+ try { socket.destroy(); } catch (e) {}
446
+ return;
447
+ }
448
+ // Started early: the session is the pool's now that it is up —
449
+ // not before, or a request that is not safe to replay would ride
450
+ // in the early data.
451
+ if (earlySession) { pool.settle(poolHost, port, earlySession); return; }
452
+ onReady();
453
+ });
454
+ // The ClientHello is built a turn after connect(): by then the socket
455
+ // knows whether it offered early data (the ticket allows it).
456
+ if (wantEarly) setTimeout(function () {
457
+ if (settled || earlySession || !socket || socket.destroyed || socket.isEarlyData !== true) return;
458
+ sentEarly = true;
459
+ earlySession = new H2Session(socket, options);
460
+ start(earlySession);
461
+ }, 0);
462
+ } else {
463
+ // h2c with prior knowledge (§3.3): no ALPN, the preface speaks for us.
464
+ socket = net.connect({ host: hostname, port: port, noDelay: options.noDelay !== false }, onReady);
465
+ }
466
+ // The request that opened the connection has the TLS socket's
467
+ // 'session' events — each a session to resume with, as Node's socket
468
+ // gives them. A request on a pooled session sees no handshake.
469
+ if (options.tls) socket.on('session', function (s) { ev.emit('session', s); });
470
+ if (options.tls) socket.on('OCSPResponse', function (r) { ev.emit('OCSPResponse', r); });
471
+ } catch (err) {
472
+ pool.settle(poolHost, port, null);
473
+ fail(err);
474
+ return;
475
+ }
476
+
477
+ socket.on('error', function (err) {
478
+ pool.settle(poolHost, port, null);
479
+ fail(err);
480
+ });
481
+ }
482
+
483
+ function start(s) {
484
+ if (settled) return;
485
+ // options.onReady (the unified client) may turn the request down here —
486
+ // another protocol was ready first and carries it: no stream is opened,
487
+ // and 'superseded' says so; the session stays in the pool.
488
+ if (typeof options.onReady === 'function' && !options.onReady()) {
489
+ settled = true;
490
+ process.nextTick(function () { ev.emit('superseded'); });
491
+ return;
492
+ }
493
+ session = s;
494
+ streamId = session.request(null, {
495
+ method: method,
496
+ onInformation: function (info) { ev.emit('information', info); },
497
+ onResponse: function (res) {
498
+ if (settled) return;
499
+ if (sentEarly) {
500
+ var st = res.socket && res.socket.earlyDataStatus ? res.socket.earlyDataStatus : (session && session.socket ? session.socket.earlyDataStatus : null);
501
+ ev.earlyData = st === 'accepted' ? 'accepted' : 'rejected';
502
+ if (res.statusCode === 425) {
503
+ var tooEarly = new Error('425 Too Early: the request is sent again after the handshake');
504
+ tooEarly.code = 'ERR_HTTP_TOO_EARLY';
505
+ tooEarly.retryable = true;
506
+ try { res.resume(); } catch (e) {}
507
+ fail(tooEarly);
508
+ return;
509
+ }
510
+ }
511
+ settled = true;
512
+ ev.emit('response', res);
513
+ if (typeof callback === 'function') { callback(res); callback = null; }
514
+ },
515
+ onError: fail
516
+ });
517
+ if (streamId === null) return;
518
+ if (ended) { sendRequest(); return; }
519
+ // Written while connecting: out now, as a stream.
520
+ if (bodyChunks.length) {
521
+ var held = bodyChunks; bodyChunks = []; bodyBuffered = 0;
522
+ var more = true;
523
+ for (var i = 0; i < held.length; i++) more = streamChunk(held[i]);
524
+ if (more && needDrain) { needDrain = false; ev.emit('drain'); }
525
+ }
526
+ }
527
+
528
+ function requestHeaderList(contentLength) {
529
+ var headers = Object.assign({}, options.headers);
530
+ if (contentLength !== null && headers['content-length'] === undefined &&
531
+ headers['Content-Length'] === undefined) {
532
+ headers['content-length'] = String(contentLength);
533
+ }
534
+ return buildRequestPseudoHeaders({
535
+ method: method,
536
+ scheme: options.tls ? 'https' : 'http',
537
+ hostname: hostname,
538
+ port: port,
539
+ path: options.path || '/',
540
+ headers: headers
541
+ });
542
+ }
543
+
544
+ // One chunk of a streamed body: HEADERS first if not yet (without a
545
+ // content-length — HTTP/2 frames the body itself, RFC 9113 §8.1.1), then
546
+ // DATA. Returns whether more may be written now.
547
+ function streamChunk(chunk) {
548
+ try {
549
+ if (!streaming) { streaming = true; sent = true; session.sendHeaders(streamId, requestHeaderList(null), false); }
550
+ var more = session.sendData(streamId, chunk, false);
551
+ if (!more) waitDrain();
552
+ return more;
553
+ } catch (err) { fail(err); return false; }
554
+ }
555
+
556
+ // The stream's send window (or the socket) has room again: 'drain'.
557
+ function waitDrain() {
558
+ needDrain = true;
559
+ if (drainHooked) return;
560
+ drainHooked = true;
561
+ var onDrain = function (id) {
562
+ if (id !== streamId) return;
563
+ session.offDrain(onDrain);
564
+ drainHooked = false;
565
+ if (needDrain) { needDrain = false; ev.emit('drain'); }
566
+ };
567
+ session.onDrain(onDrain);
568
+ }
569
+
570
+ // The whole request at once — ended before the stream existed, or with
571
+ // nothing streamed: a content-length, as before, and the body (if any).
572
+ function sendRequest() {
573
+ if (sent || streamId === null) return;
574
+ sent = true;
575
+ var body = bodyChunks.length ? Buffer.concat(bodyChunks) : null;
576
+ bodyChunks = []; bodyBuffered = 0;
577
+ try {
578
+ session.sendHeaders(streamId, requestHeaderList(body ? body.length : null), !body && !reqTrailers);
579
+ if (body) session.sendData(streamId, body, !reqTrailers);
580
+ if (reqTrailers) session.sendTrailers(streamId, reqTrailers);
581
+ } catch (err) {
582
+ fail(err);
583
+ }
584
+ }
585
+
586
+ var clientReq = {
587
+ on: function (n, f) { ev.on(n, f); return clientReq; },
588
+ once: function (n, f) { ev.once(n, f); return clientReq; },
589
+ emit: function () { ev.emit.apply(ev, arguments); return clientReq; },
590
+
591
+ write: function (chunk, encoding) {
592
+ if (ended || settled) return false;
593
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding || 'utf8');
594
+ if (!chunk || chunk.length === 0) return !needDrain;
595
+ if (streamId === null) {
596
+ bodyChunks.push(chunk);
597
+ bodyBuffered += chunk.length;
598
+ if (bodyBuffered >= BODY_HIGH_WATER) { needDrain = true; return false; }
599
+ return true;
600
+ }
601
+ return streamChunk(chunk);
602
+ },
603
+
604
+ // RFC 9218: the `priority` header while HEADERS have not gone, a
605
+ // PRIORITY_UPDATE after.
606
+ setPriority: function (p) {
607
+ if (sent || streaming) { if (session && streamId !== null && !settled) session.sendPriorityUpdate(streamId, p); return; }
608
+ var h = options.headers || (options.headers = {});
609
+ for (var k in h) if (k.toLowerCase() === 'priority') delete h[k];
610
+ var v = formatPriority(p);
611
+ if (v) h.priority = v;
612
+ },
613
+
614
+ // `trailers` (node:http's addTrailers, through the unified client): a
615
+ // HEADERS frame after the body, with END_STREAM (RFC 9113 §8.1).
616
+ end: function (chunk, encoding, trailers) {
617
+ if (ended) return clientReq;
618
+ if (encoding && typeof encoding === 'object') { trailers = encoding; encoding = undefined; }
619
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding || 'utf8');
620
+ reqTrailers = trailerFields(trailers);
621
+ if (streaming) {
622
+ // The body went as it was written: its last piece, and END_STREAM —
623
+ // on the trailers when there are some.
624
+ ended = true;
625
+ try {
626
+ if (reqTrailers) {
627
+ if (chunk && chunk.length) session.sendData(streamId, chunk, false);
628
+ session.sendTrailers(streamId, reqTrailers);
629
+ } else session.sendData(streamId, chunk && chunk.length ? chunk : null, true);
630
+ } catch (err) { fail(err); }
631
+ return clientReq;
632
+ }
633
+ if (chunk && chunk.length) { bodyChunks.push(chunk); bodyBuffered += chunk.length; }
634
+ ended = true;
635
+ if (streamId !== null) sendRequest();
636
+ return clientReq;
637
+ },
638
+
639
+ abort: function () {
640
+ if (session && streamId !== null) session.cancel(streamId);
641
+ fail(new Error('Request aborted'));
642
+ },
643
+ destroy: function (err) {
644
+ if (session && streamId !== null) session.cancel(streamId);
645
+ fail(err || new Error('Request destroyed'));
646
+ },
647
+
648
+ setTimeout: function (ms, cb) {
649
+ if (cb) ev.once('timeout', cb);
650
+ if (ms > 0) {
651
+ var t = setTimeout(function () { ev.emit('timeout'); }, ms);
652
+ if (t.unref) t.unref();
653
+ }
654
+ return clientReq;
655
+ },
656
+
657
+ get session() { return session; },
658
+ // 0-RTT: 'accepted', 'rejected', or null (not sent early).
659
+ get earlyData() { return ev.earlyData; }
660
+ };
661
+
662
+ process.nextTick(function () { if (!settled) connect(); });
663
+
664
+ return clientReq;
665
+ }
666
+
667
+
668
+ function get(options, callback) {
669
+ var req = request(Object.assign({}, options, { method: 'GET' }), callback);
670
+ req.end();
671
+ return req;
672
+ }
673
+
674
+
675
+ export { request, get, H2Pool, H2Session, globalPool };