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.
package/src/client.js CHANGED
@@ -12,13 +12,15 @@
12
12
  * https://github.com/colocohen/quico
13
13
  */
14
14
 
15
- import nodeHttps from 'node:https';
16
- import nodeHttp2 from 'node:http2';
17
15
  import { URL } from 'node:url';
16
+ import lemonTls from 'lemon-tls';
18
17
 
19
18
  import { request as h3Request } from './h3_client.js';
19
+ import { request as h2Request, H2Pool } from './h2_client.js';
20
+ import { request as h1Request, H1Pool } from './h1_client.js';
20
21
  import { ClientRequest, IncomingMessage } from './streams.js';
21
- import { globalAgent } from './agent.js';
22
+ import { creditAsRead } from './utils.js';
23
+ import { globalAgent, tlsIdentity } from './agent.js';
22
24
 
23
25
 
24
26
  // ============================================================
@@ -32,6 +34,37 @@ import { globalAgent } from './agent.js';
32
34
  // options.http1 (default: agent.http1 or true)
33
35
  // ============================================================
34
36
 
37
+ /**
38
+ * The agent's H2 and H1 pools, created on first use.
39
+ *
40
+ * They are deliberately different objects with different shapes — see the note
41
+ * at the top of h2_client.js. H2 maps an origin to ONE session and gets
42
+ * concurrency from streams; H1 maps an origin to a LIST of sockets and gets
43
+ * concurrency from more sockets. agent.js already kept _h2Pool and _h3Pool
44
+ * separate for the same reason.
45
+ */
46
+ function agentH2Pool(agent) {
47
+ if (!agent._h2ClientPool) agent._h2ClientPool = new H2Pool({});
48
+ return agent._h2ClientPool;
49
+ }
50
+
51
+ function agentH1Pool(agent) {
52
+ if (!agent._h1ClientPool) {
53
+ // node:http.Agent's options, with Node's meanings. keepAliveMsecs used to
54
+ // be passed as the idle timeout; in Node it is the TCP keep-alive delay,
55
+ // and how long a free socket is kept is the agent's `timeout`.
56
+ agent._h1ClientPool = new H1Pool({
57
+ maxSockets: agent.maxSockets,
58
+ keepAlive: agent.keepAlive,
59
+ maxFreeSockets: agent.maxFreeSockets,
60
+ keepAliveMsecs: agent.keepAliveMsecs,
61
+ idleTimeout: agent.timeout != null ? agent.timeout : undefined
62
+ });
63
+ }
64
+ return agent._h1ClientPool;
65
+ }
66
+
67
+
35
68
  function request(url, options, callback) {
36
69
 
37
70
  // ---- Normalize arguments (same overloads as node:https) ----
@@ -72,6 +105,56 @@ function request(url, options, callback) {
72
105
  var path = options.path || '/';
73
106
  var headers = options.headers || {};
74
107
  var agent = options.agent !== undefined ? options.agent : globalAgent;
108
+ // The agent's connections, to reuse — those opened with this request's TLS
109
+ // identity (ca, client certificate, rejectUnauthorized, servername), as
110
+ // node:https names them (agent.js tlsIdentity): a connection opened without
111
+ // verification, or with another certificate, does not carry this one.
112
+ // The key-exchange groups to offer (tls.connect's ecdhCurve): the
113
+ // request's, or its agent's. Checked now — a name not implemented throws
114
+ // here, as tls.connect does — and part of which connections it may share.
115
+ if (options.ecdhCurve == null && agent && agent.ecdhCurve != null) options = Object.assign({}, options, { ecdhCurve: agent.ecdhCurve });
116
+ if (options.ecdhCurve != null) lemonTls.parseEcdhCurve(options.ecdhCurve);
117
+ // The signature schemes to offer (tls.connect's sigalgs), the same way.
118
+ if (options.sigalgs == null && agent && agent.sigalgs != null) options = Object.assign({}, options, { sigalgs: agent.sigalgs });
119
+ if (options.sigalgs != null) lemonTls.parseSigalgs(options.sigalgs);
120
+ var pool = agent;
121
+ var poolHost = hostname + '#' + tlsIdentity(options);
122
+
123
+ // The TLS session to resume with on a new connection over `transport`
124
+ // ('quic' or 'tcp'): the caller's `session` (https.request's), or the
125
+ // agent's latest for this origin — and the agent keeps each new one the
126
+ // server sends (the request's 'session' event), as node:https.Agent does.
127
+ // A session the handshake then fails with is dropped (Node's
128
+ // _evictSession), so the next connection starts afresh.
129
+ //
130
+ // Taken from the agent only when a connection is actually made (the
131
+ // getter, read by the transport at connect): a request on a pooled
132
+ // connection uses none, and each session is used once (agent.js). TCP's
133
+ // are kept per ALPN — 'tcp:h2', 'tcp:http/1.1' — since each path offers
134
+ // one, and a ticket can carry 0-RTT only for the protocol it was issued
135
+ // under: an HTTP/2 ticket offered to the HTTP/1.1 path resumed without it.
136
+ // Each session the server sends is also the request's 'session' event, as
137
+ // on node:https.
138
+ function tlsSession(transport, sessPort) {
139
+ var key = (pool && typeof pool._sessionKey === 'function') ? pool._sessionKey(transport, hostname, sessPort, options) : null;
140
+ var taken = false, offered;
141
+ function take() {
142
+ if (options.session) return options.session;
143
+ if (!taken) { taken = true; offered = key ? (pool._takeSession ? pool._takeSession(key) : pool._getSession(key)) : undefined; }
144
+ return offered;
145
+ }
146
+ return {
147
+ // Defines `session` on the transport's options, read at connect.
148
+ into: function (o) { Object.defineProperty(o, 'session', { configurable: true, enumerable: true, get: take }); return o; },
149
+ keep: function (req) {
150
+ req.on('session', function (s) {
151
+ if (key) pool._cacheSession(key, s);
152
+ clientReq.emit('session', s);
153
+ });
154
+ },
155
+ failed: function () { if (key && taken && offered && !options.session) pool._evictSession(key); }
156
+ };
157
+ }
75
158
 
76
159
  // ---- Resolve protocol flags: options → agent → true ----
77
160
  var enableH2 = options.http2 !== undefined ? options.http2
@@ -85,6 +168,31 @@ function request(url, options, callback) {
85
168
  || (agent && agent.h3Timeout)
86
169
  || 3000;
87
170
 
171
+ // ---- Encrypted Client Hello (RFC 9849) ----
172
+ // options.ech: the server's ECHConfigList (bytes, or base64 — the `ech=`
173
+ // of its DNS HTTPS record): the real ClientHello, with the name, goes
174
+ // encrypted, on every version. Rejected by a server that sent new keys,
175
+ // the request is tried once more with them (it was never sent: ECH fails
176
+ // before the handshake completes). Without a config, GREASE ECH — as the
177
+ // browsers send, so that a client with one does not stand out — unless
178
+ // echGrease is false.
179
+ //
180
+ // The keys a server sent on rejecting the caller's (retry_configs) are
181
+ // kept by the agent (_getEchRetry) and used first on the next requests
182
+ // given the same config: a server that rotated its keys cost a rejection
183
+ // and a retry on every connection until the caller's DNS had the new
184
+ // ones. Kept keys rejected in their turn are forgotten, and the caller's
185
+ // config tried again — whose rejection may bring new keys; a rejection of
186
+ // a connection made with retry_configs never does (RFC 9849 §6.1.6:
187
+ // "Clients SHOULD NOT accept retry_config in response to a connection
188
+ // initiated in response to a retry_config").
189
+ var echOriginal = options.ech || null;
190
+ var echKey = (echOriginal && agent && typeof agent._getEchRetry === 'function') ? agent._sessionKey('ech', hostname, port, options) : null;
191
+ var echKept = echKey ? agent._getEchRetry(echKey) : null;
192
+ var echConfig = echKept || echOriginal;
193
+ var echRetried = false;
194
+ var echGrease = !echConfig && options.echGrease !== false;
195
+
88
196
  // ---- Create ClientRequest (returned immediately) ----
89
197
  var clientReq = new ClientRequest({
90
198
  method: method,
@@ -95,25 +203,55 @@ function request(url, options, callback) {
95
203
  _responseCallback: callback
96
204
  });
97
205
 
206
+ // RFC 9218: the request's priority, from the start (setPriority changes it).
207
+ if (options.priority) clientReq.setPriority(options.priority);
208
+
98
209
  // Track if response was already delivered (prevent double-delivery)
99
210
  var responded = false;
100
211
  var _fallbackTimer = null;
101
212
 
102
- // ---- Wait for body if needed, then connect ----
103
- var bodyReady = (method === 'GET' || method === 'HEAD');
104
-
105
- if (!bodyReady) {
106
- clientReq.once('_bodyReady', function () {
107
- bodyReady = true;
108
- maybeConnect();
109
- });
213
+ // ---- Connect at once; the body follows as it is written ----
214
+ // It waited for end(): a body was held whole in memory, and nothing went
215
+ // out — not even the connection was begun — until it was complete.
216
+ process.nextTick(function () { doProtocolSelection(); });
217
+
218
+ // ---- The transport that carries the request ----
219
+ // Several may be tried at once (HTTP/3, and TCP after h3Timeout), and the
220
+ // first to be ready to send takes the request (claim, from each
221
+ // transport's onReady): the body goes to it, and to it only. One ready
222
+ // later is turned down and sends nothing ('superseded'). Both used to
223
+ // send it — the timer that starts TCP ran until the RESPONSE, so a POST
224
+ // whose answer took HTTP/3 more than h3Timeout went to the server twice.
225
+ var owner = null;
226
+ function attempt(name) { return { name: name, req: null }; }
227
+ function claim(token) {
228
+ if (responded || clientReq.destroyed || clientReq.aborted) return false;
229
+ if (owner) return owner === token;
230
+ owner = token;
231
+ // HTTP/3 has it: no TCP attempt is needed any more.
232
+ if (token.name === 'h3' && _fallbackTimer) { clearTimeout(_fallbackTimer); _fallbackTimer = null; }
233
+ if (token.req) clientReq._attachSink(token.req);
234
+ return true;
110
235
  }
111
-
112
- process.nextTick(function () { maybeConnect(); });
113
-
114
- function maybeConnect() {
115
- if (method !== 'GET' && method !== 'HEAD' && !bodyReady) return;
116
- doProtocolSelection();
236
+ // The transport's request exists (claim may have come from inside it,
237
+ // a pooled connection being ready at once): the body to it if it has it.
238
+ function bind(token, req) {
239
+ token.req = req;
240
+ if (owner === token) clientReq._attachSink(req);
241
+ }
242
+ // The attempt failed. If it carried the request, the body comes back for
243
+ // another — unless more of it went than was kept (ClientRequest's
244
+ // REPLAY_MAX): then no other can carry it, and the request fails.
245
+ function release(token) {
246
+ if (owner !== token) return true;
247
+ if (!clientReq._detachSink()) return false;
248
+ owner = null;
249
+ return true;
250
+ }
251
+ function failOver(token, err, again, onFail) {
252
+ if (!release(token)) { if (!responded) clientReq.emit('error', err); return; }
253
+ if (again && retryUnprocessed(err, again)) return;
254
+ onFail(err);
117
255
  }
118
256
 
119
257
 
@@ -124,6 +262,26 @@ function request(url, options, callback) {
124
262
  function doProtocolSelection() {
125
263
  var cached = agent ? agent.getProtocol(hostname, port) : null;
126
264
 
265
+ // The origin advertised HTTP/3 (Alt-Svc): try it first, at the advertised
266
+ // port, whatever worked here before — that is how a client that met the
267
+ // origin over TCP moves to HTTP/3. Should the alternative fail, it is
268
+ // marked broken for a while and the request goes on over TCP.
269
+ var alt = agent && agent.getAltSvc ? agent.getAltSvc(hostname, port) : null;
270
+ if (alt && alt.port !== port) {
271
+ var tryAlt = function (onFail) {
272
+ tryH3(function (err) { agent.markAltSvcBroken(hostname, port); onFail(err); }, alt.port);
273
+ };
274
+ // Then the origin itself: over TCP as it last worked, and over HTTP/3 at
275
+ // its own port — an origin that answered over HTTP/3 may have sent the
276
+ // advertisement.
277
+ var rest = [];
278
+ if (cached === 'https' && enableH1) { rest.push(tryHttps); if (enableH2) rest.push(tryH2); }
279
+ else { if (enableH2) rest.push(tryH2); if (enableH1) rest.push(tryHttps); }
280
+ if (cached === 'h3') rest.unshift(tryH3); else rest.push(tryH3);
281
+ runChainWithH3Timeout([tryAlt].concat(rest), h3Timeout);
282
+ return;
283
+ }
284
+
127
285
  // Build ordered fallback chain
128
286
  // H3 is always first (that's the point of quico)
129
287
  var chain = [tryH3];
@@ -146,20 +304,34 @@ function request(url, options, callback) {
146
304
  /**
147
305
  * Run through the fallback chain. If step N fails, try step N+1.
148
306
  */
149
- function runChain(chain, index) {
307
+ function runChain(chain, index, lastErr) {
150
308
  if (responded || clientReq.destroyed || clientReq.aborted) return;
151
309
  if (index >= chain.length) {
152
- // All protocols failed
153
- clientReq.emit('error', new Error('All protocols failed for ' + hostname + ':' + port));
310
+ // All protocols failed — the last one's reason as the `cause`; it was
311
+ // dropped, and "all protocols failed" was all a caller could learn.
312
+ clientReq.emit('error', new Error('All protocols failed for ' + hostname + ':' + port, lastErr ? { cause: lastErr } : undefined));
154
313
  return;
155
314
  }
156
315
 
157
316
  chain[index](function onFail(err) {
158
317
  if (responded) return;
159
- runChain(chain, index + 1);
318
+ if (isRequestError(err)) { clientReq.emit('error', err); return; }
319
+ runChain(chain, index + 1, err);
160
320
  });
161
321
  }
162
322
 
323
+ // An error about the request itself — not about reaching the server over
324
+ // one protocol — fails the request as it is: another protocol would only
325
+ // send the same faulty request again, and the caller would be told "all
326
+ // protocols failed" instead of what is wrong.
327
+ // ECH rejected (after the one retry with new keys, or with none offered)
328
+ // ends the request with that reason: the same server, proven for its
329
+ // public name, would decline it over every protocol — and "all protocols
330
+ // failed" would hide why.
331
+ function isRequestError(err) {
332
+ return !!(err && (err.requestError || err.code === 'ERR_ECH_REJECTED'));
333
+ }
334
+
163
335
  /**
164
336
  * Special first-run: try H3 with a timeout.
165
337
  * If H3 doesn't connect in time, start fallbacks in parallel.
@@ -182,12 +354,13 @@ function request(url, options, callback) {
182
354
  _fallbackTimer = timer;
183
355
  if (timer.unref) timer.unref(); // Don't prevent process exit
184
356
 
185
- // Try H3
186
- tryH3(function onFail(err) {
357
+ // Try H3 (the chain's first step)
358
+ chain[0](function onFail(err) {
187
359
  clearTimeout(timer);
360
+ if (isRequestError(err) && !responded) { h3Done = true; clientReq.emit('error', err); return; }
188
361
  if (!h3Done && !responded) {
189
362
  h3Done = true;
190
- runChain(chain.slice(1), 0);
363
+ runChain(chain.slice(1), 0, err);
191
364
  }
192
365
  });
193
366
  }
@@ -197,140 +370,311 @@ function request(url, options, callback) {
197
370
  // H3 transport (wraps existing h3_client.js)
198
371
  // ============================================================
199
372
 
200
- function tryH3(onFail) {
373
+ // h3Port: where to try HTTP/3 — the origin's own port unless an Alt-Svc
374
+ // alternative is being tried.
375
+ // A request the server did not process — refused (HTTP/2's
376
+ // REFUSED_STREAM, HTTP/3's H3_REQUEST_REJECTED) or past its GOAWAY — may be
377
+ // sent again (RFC 9113 §8.7, RFC 9114 §4.1.1, §5.2): once, on the same
378
+ // protocol, so a new connection. It fell to the next protocol in the
379
+ // chain, or with none left failed — a server that closes its connections
380
+ // after N requests (maxRequestsPerSocket) failed the requests in flight
381
+ // as it did. The body is back here (release), whole — or the request fails.
382
+ var retriedUnprocessed = false;
383
+ // TLS early data (HTTP/2, HTTP/1.1): what became of it — the transport's
384
+ // word, or 'rejected' when a 425 sent the request again after the handshake.
385
+ var tooEarlyRetried = false;
386
+ function earlyOutcome(transportReq) {
387
+ if (tooEarlyRetried) return 'rejected';
388
+ return transportReq && transportReq.earlyData ? transportReq.earlyData : null;
389
+ }
390
+
391
+ function retryUnprocessed(err, again) {
392
+ if (err && err.code === 'ERR_HTTP_TOO_EARLY') tooEarlyRetried = true;
393
+ // ECH rejected, with the server's current keys (lemon-tls's
394
+ // ERR_ECH_REJECTED): once more, with them. A rejection without keys is
395
+ // the server's ECH turned off — going on without ECH is the caller's
396
+ // decision, so it fails.
397
+ if (err && err.code === 'ERR_ECH_REJECTED' && echKept &&
398
+ !responded && !clientReq.destroyed && !clientReq.aborted) {
399
+ agent._evictEchRetry(echKey);
400
+ echKept = null;
401
+ echConfig = echOriginal;
402
+ process.nextTick(function () {
403
+ if (responded || clientReq.destroyed || clientReq.aborted) return;
404
+ again();
405
+ });
406
+ return true;
407
+ }
408
+ if (err && err.code === 'ERR_ECH_REJECTED' && err.retryConfigs && !echRetried &&
409
+ !responded && !clientReq.destroyed && !clientReq.aborted) {
410
+ echRetried = true;
411
+ echConfig = err.retryConfigs;
412
+ process.nextTick(function () {
413
+ if (responded || clientReq.destroyed || clientReq.aborted) return;
414
+ again();
415
+ });
416
+ return true;
417
+ }
418
+ if (!err || !err.retryable || retriedUnprocessed) return false;
419
+ if (responded || clientReq.destroyed || clientReq.aborted) return false;
420
+ retriedUnprocessed = true;
421
+ // After the event that failed it has reached every listener — the
422
+ // agent's among them, which takes the connection out of its pool; run
423
+ // inside it, the retry was handed the same GOAWAY'd connection.
424
+ process.nextTick(function () {
425
+ if (responded || clientReq.destroyed || clientReq.aborted) return;
426
+ again();
427
+ });
428
+ return true;
429
+ }
430
+
431
+ // res.socket over HTTP/3: there is no TLS socket — the handshake ran in
432
+ // the QUIC connection — so what an application asks a TLSSocket for here
433
+ // is answered from that connection: as over HTTP/2 and HTTP/1.1, whose
434
+ // res.socket is the lemon-tls socket.
435
+ function h3SocketInfo(conn) {
436
+ var q = conn && conn.quic;
437
+ if (!q) return null;
438
+ return {
439
+ encrypted: true,
440
+ alpnProtocol: 'h3',
441
+ get remoteAddress() { return q.remoteAddress || null; },
442
+ get remotePort() { return q.remotePort || null; },
443
+ get echStatus() { return q.echStatus; },
444
+ get echAccepted() { return q.echStatus === 'accepted'; },
445
+ isSessionReused: function () { return !!(q.isSessionReused && q.isSessionReused()); },
446
+ getPeerCertificate: function () { return q.getPeerCertificate ? q.getPeerCertificate() : {}; },
447
+ getNegotiationResult: function () { return q.getNegotiationResult ? q.getNegotiationResult() : null; },
448
+ // The connection's options while it runs (quic_connection's
449
+ // setOptions) — shared by every request on it (a pooled connection).
450
+ setOptions: function (o) { q.setOptions(o); return this; },
451
+ getOptions: function () { return q.getOptions(); }
452
+ };
453
+ }
454
+
455
+ function tryH3(onFail, h3Port) {
201
456
  if (responded || clientReq.destroyed || clientReq.aborted) return;
457
+ h3Port = h3Port || port;
202
458
 
203
459
  try {
204
460
  var h3Opts = {
205
461
  hostname: hostname,
206
- port: port,
462
+ port: h3Port,
207
463
  path: path,
208
464
  method: method,
209
- headers: Object.assign({}, headers)
465
+ headers: Object.assign({}, headers),
466
+ // The H2 and HTTPS fallbacks below honour this; dropping it here made
467
+ // H3 — alone of the three — unable to talk to self-signed or
468
+ // private-CA servers, and every such request silently fell back to
469
+ // TCP (or hung when the server was QUIC-only).
470
+ rejectUnauthorized: options.rejectUnauthorized,
471
+ ca: options.ca,
472
+ // The name for SNI and the certificate check, as over TCP (tls.connect's
473
+ // servername) — HTTP/3 used the hostname whatever was given, so a
474
+ // request to an address with a name of its own worked over TCP alone.
475
+ servername: options.servername,
476
+ key: options.key, cert: options.cert, passphrase: options.passphrase,
477
+ requestOCSP: options.requestOCSP === true,
478
+ ech: echConfig, echGrease: echGrease, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs,
479
+ startupCap: options.startupCap, pmtuDiscovery: options.pmtuDiscovery,
480
+ maxUdpPayloadSize: options.maxUdpPayloadSize, initialRtt: options.initialRtt,
481
+ reorderWindow: options.reorderWindow, initialCongestionWindow: options.initialCongestionWindow,
482
+ // 0-RTT with the saved session (below), off unless asked for.
483
+ earlyData: options.earlyData === true
210
484
  };
485
+ h3Opts.quicVersions = options.quicVersions;
486
+ var sess = tlsSession('quic', h3Port);
487
+ sess.into(h3Opts);
488
+ // The server's NEW_TOKEN token from the last connection, used once.
489
+ var tokenKey = (pool && typeof pool._takeToken === 'function') ? pool._sessionKey('quic', hostname, h3Port, options) : null;
211
490
 
212
491
  // Check Agent for existing QUIC connection (multiplexing).
213
492
  // With an agent, the connection's lifecycle is owned by the pool
214
493
  // (sweep / idle timer / agent.destroy), so signal h3_client not to
215
494
  // auto-close it when this request finishes — otherwise the first
216
495
  // request would tear down a connection still pooled for reuse.
217
- if (agent) {
496
+ if (pool) {
218
497
  h3Opts._managed = true;
219
- var existing = agent.getH3Connection(hostname, port);
220
- if (existing) h3Opts._connection = existing;
498
+ var existing = pool.getH3Connection(poolHost, h3Port);
499
+ if (existing) {
500
+ h3Opts._connection = existing;
501
+ } else if (pool.joinH3Pending &&
502
+ pool.joinH3Pending(poolHost, h3Port, function () {
503
+ // The lead settled. Retry from the top: on success the
504
+ // pool lookup now hits and we multiplex on the lead's
505
+ // connection; on failure we become the next lead — a
506
+ // serial retry, never a thundering herd.
507
+ tryH3(onFail, h3Port);
508
+ })) {
509
+ return; // parked as a follower until the lead settles
510
+ }
511
+ // else: we are the LEAD for this origin. _settleH3Pending below MUST
512
+ // run on every exit path, or followers park forever.
221
513
  }
222
514
 
515
+ var _pendingLead = !!(pool && !h3Opts._connection);
516
+ function _settleH3Pending(conn) {
517
+ if (!_pendingLead) return;
518
+ _pendingLead = false;
519
+ if (pool.resolveH3Pending) pool.resolveH3Pending(poolHost, h3Port, conn);
520
+ else if (conn) pool.setH3Connection(poolHost, h3Port, conn);
521
+ }
522
+
523
+ if (tokenKey && !h3Opts._connection) h3Opts.token = pool._takeToken(tokenKey);
524
+ var token = attempt('h3');
525
+ h3Opts.onReady = function () { return claim(token); };
223
526
  var h3Req = h3Request(h3Opts, function (h3Res) {
224
527
  if (responded) return;
225
528
 
226
- // Store connection for future reuse
227
- if (agent && h3Req._connection && !h3Opts._connection) {
228
- agent.setH3Connection(hostname, port, h3Req._connection);
529
+ // Register the connection and wake coalesced followers.
530
+ if (h3Req._connection && !h3Opts._connection) {
531
+ _settleH3Pending(h3Req._connection);
229
532
  }
230
533
 
231
534
  var res = new IncomingMessage({
232
535
  httpVersion: '3.0',
233
536
  statusCode: h3Res.statusCode,
234
537
  statusMessage: '',
235
- headers: h3Res.headers || {}
538
+ headers: h3Res.headers || {},
539
+ socket: h3SocketInfo(h3Req._connection || h3Opts._connection)
236
540
  });
541
+ // 0-RTT: 'accepted', 'rejected' (it went again after the handshake), or null.
542
+ res.earlyData = h3Res.earlyData != null ? h3Res.earlyData : null;
237
543
 
238
544
  clientReq.protocol = 'h3';
239
- if (agent) agent.setProtocol(hostname, port, 'h3');
545
+ // "HTTP/3 works at the origin's own port" — not true of an alternative
546
+ // elsewhere, which Alt-Svc already records.
547
+ if (agent && h3Port === port) agent.setProtocol(hostname, port, 'h3');
548
+ // An HTTP/3 response may carry Alt-Svc too (a change, or `clear`).
549
+ if (agent && h3Res.headers && h3Res.headers['alt-svc']) {
550
+ agent.parseAltSvc(hostname, port, h3Res.headers['alt-svc']);
551
+ }
240
552
 
241
- h3Res.on('data', function (chunk) { res._pushData(chunk); });
242
- h3Res.on('end', function () { res._endData(); });
553
+ // Credit for what the application has read, not for what arrived
554
+ // (utils' creditAsRead), as the server does with request bodies: the
555
+ // server sends no faster than this response is consumed.
556
+ var received = null;
557
+ if (typeof h3Res._holdBody === 'function') {
558
+ h3Res._holdBody();
559
+ received = creditAsRead(res, function (n) { h3Res._creditBody(n); });
560
+ res.on('close', function () { h3Res._releaseBody(); });
561
+ }
562
+ h3Res.on('data', function (chunk) { if (received) received(chunk.length); res._pushData(chunk); });
563
+ h3Res.on('end', function () {
564
+ // Trailers arrive before the end (gRPC's grpc-status): on res by 'end'.
565
+ if (h3Res.trailers && Object.keys(h3Res.trailers).length) {
566
+ res.trailers = h3Res.trailers;
567
+ res.rawTrailers = h3Res.rawTrailers || [];
568
+ }
569
+ res._endData();
570
+ });
243
571
  h3Res.on('error', function (err) { res._abort(err); });
244
572
 
245
573
  deliverResponse(res);
246
574
  });
247
575
 
576
+ sess.keep(h3Req);
577
+ if (tokenKey) h3Req.on('newToken', function (t) { pool._cacheToken(tokenKey, t); });
578
+ // tls.connect's requestOCSP: the server's stapled response, on this request.
579
+ h3Req.on('OCSPResponse', function (r) { clientReq.emit('OCSPResponse', r); });
580
+ // 1xx (103 Early Hints): node:http's 'information', on every version.
581
+ h3Req.on('information', function (info) { clientReq.emit('information', info); });
248
582
  h3Req.on('error', function (err) {
249
- onFail(err);
583
+ if (!h3Opts._connection && !err.retryable) sess.failed();
584
+ _settleH3Pending(null); // release followers before failing over
585
+ failOver(token, err, function () { tryH3(onFail, h3Port); }, onFail);
586
+ });
587
+ // Another protocol had the request first: this connection is kept for
588
+ // the pool (and the requests parked behind it), and nothing is sent.
589
+ h3Req.on('superseded', function () { _settleH3Pending(h3Req._connection || null); });
590
+ // req.destroy(): cancel this attempt's stream. A pool lead hands its
591
+ // connection (if it has one yet) to the requests parked behind it.
592
+ clientReq._onCancel(function () {
593
+ _settleH3Pending(h3Req._connection || null);
594
+ h3Req.destroy();
250
595
  });
251
596
 
252
- var body = clientReq._getBody();
253
- if (body) h3Req.write(body);
254
- h3Req.end();
597
+ bind(token, h3Req);
255
598
 
256
599
  } catch (err) {
600
+ if (typeof _settleH3Pending === 'function') _settleH3Pending(null);
257
601
  onFail(err);
258
602
  }
259
603
  }
260
604
 
261
605
 
262
606
  // ============================================================
263
- // H2 transport (node:http2)
607
+ // H2 transport (ours)
608
+ //
609
+ // Was node:http2. h2_client speaks the same shape — an emitter that emits
610
+ // 'response' with an IncomingMessage — so the fallback chain around it is
611
+ // unchanged. Sessions are pooled and coalesced per origin by the agent, the
612
+ // same way H3 connections are.
264
613
  // ============================================================
265
614
 
615
+ // agent: false (or null) — node:https's meaning: a connection of this
616
+ // request's own, closed when it is done. HTTP/2 and HTTP/1.1 fell back
617
+ // to their modules' shared pools instead: a request with agent: false
618
+ // went on another's connection, kept open after it (and its `session`
619
+ // option, read only when a connection is made, was never used).
620
+ function oneOffH2() {
621
+ var p = new H2Pool({});
622
+ function done() { setImmediate(function () { p.destroy(); }); }
623
+ clientReq.once('response', function (res) { res.once('end', done); res.once('close', done); });
624
+ clientReq.once('error', done);
625
+ clientReq.once('close', done);
626
+ return p;
627
+ }
628
+
266
629
  function tryH2(onFail) {
267
630
  if (responded || clientReq.destroyed || clientReq.aborted) return;
268
631
 
269
632
  try {
270
- var session = agent ? agent.getH2Session(hostname, port) : null;
271
-
272
- if (!session) {
273
- var authority = 'https://' + hostname + ':' + port;
274
- session = nodeHttp2.connect(authority, {
275
- rejectUnauthorized: options.rejectUnauthorized !== false
276
- });
277
-
278
- session.on('error', function (err) {
279
- onFail(err);
280
- });
281
-
282
- if (agent) agent.setH2Session(hostname, port, session);
283
- }
284
-
285
- var h2Headers = {
286
- ':method': method,
287
- ':path': path,
288
- ':authority': hostname,
289
- ':scheme': 'https'
290
- };
291
- for (var name in headers) {
292
- if (name[0] !== ':') h2Headers[name.toLowerCase()] = headers[name];
293
- }
294
-
295
- var h2Req = session.request(h2Headers);
296
-
297
- h2Req.on('response', function (responseHeaders) {
633
+ var sess = tlsSession('tcp:h2', port);
634
+ var token = attempt('h2');
635
+ var h2Req = h2Request(sess.into({
636
+ onReady: function () { return claim(token); },
637
+ hostname: hostname,
638
+ port: port,
639
+ path: path,
640
+ method: method,
641
+ headers: Object.assign({}, headers),
642
+ tls: lemonTls,
643
+ ca: options.ca,
644
+ rejectUnauthorized: options.rejectUnauthorized,
645
+ servername: options.servername,
646
+ key: options.key, cert: options.cert, passphrase: options.passphrase,
647
+ requestOCSP: options.requestOCSP === true,
648
+ ech: echConfig, echGrease: echGrease, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs,
649
+ // One session per origin, shared with every other request through this
650
+ // agent — that is what multiplexing means and why the H1 pool cannot
651
+ // be reused here.
652
+ pool: pool ? agentH2Pool(pool) : oneOffH2(),
653
+ // 0-RTT over TLS (see h2_client); not again after a 425.
654
+ earlyData: options.earlyData === true && !tooEarlyRetried
655
+ }), function (res) {
298
656
  if (responded) return;
299
657
 
300
- var statusCode = responseHeaders[':status'] || 200;
301
- var flatHeaders = {};
302
- for (var k in responseHeaders) {
303
- if (k[0] !== ':') flatHeaders[k] = responseHeaders[k];
304
- }
305
-
306
- // Alt-Svc discovery
307
- if (agent && flatHeaders['alt-svc']) {
308
- agent.parseAltSvc(hostname, port, flatHeaders['alt-svc']);
658
+ if (agent && res.headers['alt-svc']) {
659
+ agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
309
660
  }
310
661
 
311
- var res = new IncomingMessage({
312
- httpVersion: '2.0',
313
- statusCode: statusCode,
314
- headers: flatHeaders
315
- });
316
-
317
662
  clientReq.protocol = 'h2';
318
663
  if (agent) agent.setProtocol(hostname, port, 'h2');
319
-
320
- h2Req.on('data', function (chunk) { res._pushData(chunk); });
321
- h2Req.on('end', function () { res._endData(); });
322
- h2Req.on('error', function (err) { res._abort(err); });
323
-
664
+ res.earlyData = earlyOutcome(h2Req);
324
665
  deliverResponse(res);
325
666
  });
326
667
 
668
+ sess.keep(h2Req);
669
+ h2Req.on('OCSPResponse', function (r) { clientReq.emit('OCSPResponse', r); });
670
+ h2Req.on('information', function (info) { clientReq.emit('information', info); });
327
671
  h2Req.on('error', function (err) {
328
- onFail(err);
672
+ if (!err.retryable) sess.failed();
673
+ failOver(token, err, function () { tryH2(onFail); }, onFail);
329
674
  });
675
+ clientReq._onCancel(function () { h2Req.destroy(); }); // RST_STREAM (CANCEL)
330
676
 
331
- var body = clientReq._getBody();
332
- if (body) h2Req.write(Buffer.from(body));
333
- h2Req.end();
677
+ bind(token, h2Req);
334
678
 
335
679
  } catch (err) {
336
680
  onFail(err);
@@ -339,53 +683,56 @@ function request(url, options, callback) {
339
683
 
340
684
 
341
685
  // ============================================================
342
- // HTTPS transport (node:https — HTTP/1.1)
686
+ // HTTPS transport (ours — HTTP/1.1 over lemon-tls)
343
687
  // ============================================================
344
688
 
345
689
  function tryHttps(onFail) {
346
690
  if (responded || clientReq.destroyed || clientReq.aborted) return;
347
691
 
348
692
  try {
349
- var httpsOpts = {
693
+ var sess = tlsSession('tcp:http/1.1', port);
694
+ var token = attempt('https');
695
+ var httpsReq = h1Request(sess.into({
696
+ onReady: function () { return claim(token); },
350
697
  hostname: hostname,
351
698
  port: port,
352
699
  path: path,
353
700
  method: method,
354
701
  headers: Object.assign({}, headers),
355
- rejectUnauthorized: options.rejectUnauthorized !== false
356
- };
357
-
358
- var httpsReq = nodeHttps.request(httpsOpts, function (httpsRes) {
702
+ tls: lemonTls,
703
+ ca: options.ca,
704
+ rejectUnauthorized: options.rejectUnauthorized,
705
+ servername: options.servername,
706
+ key: options.key, cert: options.cert, passphrase: options.passphrase,
707
+ requestOCSP: options.requestOCSP === true,
708
+ ech: echConfig, echGrease: echGrease, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs,
709
+ pool: pool ? agentH1Pool(pool) : new H1Pool({ keepAlive: false }),
710
+ earlyData: options.earlyData === true && !tooEarlyRetried
711
+ }), function (res) {
359
712
  if (responded) return;
360
713
 
361
- if (agent && httpsRes.headers['alt-svc']) {
362
- agent.parseAltSvc(hostname, port, httpsRes.headers['alt-svc']);
714
+ if (agent && res.headers['alt-svc']) {
715
+ agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
363
716
  }
364
717
 
365
- var res = new IncomingMessage({
366
- httpVersion: httpsRes.httpVersion || '1.1',
367
- statusCode: httpsRes.statusCode,
368
- statusMessage: httpsRes.statusMessage || '',
369
- headers: httpsRes.headers || {}
370
- });
371
-
372
718
  clientReq.protocol = 'https';
373
719
  if (agent) agent.setProtocol(hostname, port, 'https');
374
-
375
- httpsRes.on('data', function (chunk) { res._pushData(chunk); });
376
- httpsRes.on('end', function () { res._endData(); });
377
- httpsRes.on('error', function (err) { res._abort(err); });
378
-
720
+ res.earlyData = earlyOutcome(httpsReq);
379
721
  deliverResponse(res);
380
722
  });
381
723
 
724
+ sess.keep(httpsReq);
725
+ httpsReq.on('OCSPResponse', function (r) { clientReq.emit('OCSPResponse', r); });
726
+ httpsReq.on('information', function (info) { clientReq.emit('information', info); });
727
+ // A retryable failure (425 Too Early to a request sent in TLS early
728
+ // data) goes again, as over HTTP/2; anything else falls over.
382
729
  httpsReq.on('error', function (err) {
383
- onFail(err);
730
+ if (!err.retryable) sess.failed();
731
+ failOver(token, err, err.retryable ? function () { tryHttps(onFail); } : null, onFail);
384
732
  });
733
+ clientReq._onCancel(function () { httpsReq.destroy(); }); // the socket goes
385
734
 
386
- var body = clientReq._getBody();
387
- if (body) httpsReq.write(Buffer.from(body));
388
- httpsReq.end();
735
+ bind(token, httpsReq);
389
736
 
390
737
  } catch (err) {
391
738
  onFail(err);
@@ -400,6 +747,15 @@ function request(url, options, callback) {
400
747
  function deliverResponse(res) {
401
748
  if (responded) return;
402
749
  responded = true;
750
+ // The server's new ECH keys worked: kept for the next requests.
751
+ if (echRetried && echKey) agent._cacheEchRetry(echKey, echConfig);
752
+ clientReq.res = res;
753
+ // The application destroying the response mid-way gives the request up
754
+ // with it — the stream cancelled (STOP_SENDING and RESET_STREAM over
755
+ // HTTP/3, RST_STREAM over HTTP/2), the socket over HTTP/1.1 — as
756
+ // node:http's res.destroy() destroys the socket. The rest of the body
757
+ // kept coming, at full speed, into a response nobody read.
758
+ if (typeof res._onDestroy === 'function') res._onDestroy(function () { if (!res.complete && !clientReq.destroyed) clientReq.destroy(); });
403
759
 
404
760
  if (clientReq._timeoutTimer) {
405
761
  clearTimeout(clientReq._timeoutTimer);