quico 0.4.0 → 0.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/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,13 +370,96 @@ 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
465
  headers: Object.assign({}, headers),
@@ -212,26 +468,43 @@ function request(url, options, callback) {
212
468
  // private-CA servers, and every such request silently fell back to
213
469
  // TCP (or hung when the server was QUIC-only).
214
470
  rejectUnauthorized: options.rejectUnauthorized,
215
- ca: options.ca
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
216
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;
217
490
 
218
491
  // Check Agent for existing QUIC connection (multiplexing).
219
492
  // With an agent, the connection's lifecycle is owned by the pool
220
493
  // (sweep / idle timer / agent.destroy), so signal h3_client not to
221
494
  // auto-close it when this request finishes — otherwise the first
222
495
  // request would tear down a connection still pooled for reuse.
223
- if (agent) {
496
+ if (pool) {
224
497
  h3Opts._managed = true;
225
- var existing = agent.getH3Connection(hostname, port);
498
+ var existing = pool.getH3Connection(poolHost, h3Port);
226
499
  if (existing) {
227
500
  h3Opts._connection = existing;
228
- } else if (agent.joinH3Pending &&
229
- agent.joinH3Pending(hostname, port, function () {
501
+ } else if (pool.joinH3Pending &&
502
+ pool.joinH3Pending(poolHost, h3Port, function () {
230
503
  // The lead settled. Retry from the top: on success the
231
504
  // pool lookup now hits and we multiplex on the lead's
232
505
  // connection; on failure we become the next lead — a
233
506
  // serial retry, never a thundering herd.
234
- tryH3(onFail);
507
+ tryH3(onFail, h3Port);
235
508
  })) {
236
509
  return; // parked as a follower until the lead settles
237
510
  }
@@ -239,14 +512,17 @@ function request(url, options, callback) {
239
512
  // run on every exit path, or followers park forever.
240
513
  }
241
514
 
242
- var _pendingLead = !!(agent && !h3Opts._connection);
515
+ var _pendingLead = !!(pool && !h3Opts._connection);
243
516
  function _settleH3Pending(conn) {
244
517
  if (!_pendingLead) return;
245
518
  _pendingLead = false;
246
- if (agent.resolveH3Pending) agent.resolveH3Pending(hostname, port, conn);
247
- else if (conn) agent.setH3Connection(hostname, port, conn);
519
+ if (pool.resolveH3Pending) pool.resolveH3Pending(poolHost, h3Port, conn);
520
+ else if (conn) pool.setH3Connection(poolHost, h3Port, conn);
248
521
  }
249
522
 
523
+ if (tokenKey && !h3Opts._connection) h3Opts.token = pool._takeToken(tokenKey);
524
+ var token = attempt('h3');
525
+ h3Opts.onReady = function () { return claim(token); };
250
526
  var h3Req = h3Request(h3Opts, function (h3Res) {
251
527
  if (responded) return;
252
528
 
@@ -259,27 +535,66 @@ function request(url, options, callback) {
259
535
  httpVersion: '3.0',
260
536
  statusCode: h3Res.statusCode,
261
537
  statusMessage: '',
262
- headers: h3Res.headers || {}
538
+ headers: h3Res.headers || {},
539
+ socket: h3SocketInfo(h3Req._connection || h3Opts._connection)
263
540
  });
541
+ // 0-RTT: 'accepted', 'rejected' (it went again after the handshake), or null.
542
+ res.earlyData = h3Res.earlyData != null ? h3Res.earlyData : null;
264
543
 
265
544
  clientReq.protocol = 'h3';
266
- 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
+ }
267
552
 
268
- h3Res.on('data', function (chunk) { res._pushData(chunk); });
269
- 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
+ });
270
571
  h3Res.on('error', function (err) { res._abort(err); });
271
572
 
272
573
  deliverResponse(res);
273
574
  });
274
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); });
275
582
  h3Req.on('error', function (err) {
583
+ if (!h3Opts._connection && !err.retryable) sess.failed();
276
584
  _settleH3Pending(null); // release followers before failing over
277
- onFail(err);
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();
278
595
  });
279
596
 
280
- var body = clientReq._getBody();
281
- if (body) h3Req.write(body);
282
- h3Req.end();
597
+ bind(token, h3Req);
283
598
 
284
599
  } catch (err) {
285
600
  if (typeof _settleH3Pending === 'function') _settleH3Pending(null);
@@ -289,77 +604,77 @@ function request(url, options, callback) {
289
604
 
290
605
 
291
606
  // ============================================================
292
- // 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.
293
613
  // ============================================================
294
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
+
295
629
  function tryH2(onFail) {
296
630
  if (responded || clientReq.destroyed || clientReq.aborted) return;
297
631
 
298
632
  try {
299
- var session = agent ? agent.getH2Session(hostname, port) : null;
300
-
301
- if (!session) {
302
- var authority = 'https://' + hostname + ':' + port;
303
- session = nodeHttp2.connect(authority, {
304
- rejectUnauthorized: options.rejectUnauthorized !== false
305
- });
306
-
307
- session.on('error', function (err) {
308
- onFail(err);
309
- });
310
-
311
- if (agent) agent.setH2Session(hostname, port, session);
312
- }
313
-
314
- var h2Headers = {
315
- ':method': method,
316
- ':path': path,
317
- ':authority': hostname,
318
- ':scheme': 'https'
319
- };
320
- for (var name in headers) {
321
- if (name[0] !== ':') h2Headers[name.toLowerCase()] = headers[name];
322
- }
323
-
324
- var h2Req = session.request(h2Headers);
325
-
326
- 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) {
327
656
  if (responded) return;
328
657
 
329
- var statusCode = responseHeaders[':status'] || 200;
330
- var flatHeaders = {};
331
- for (var k in responseHeaders) {
332
- if (k[0] !== ':') flatHeaders[k] = responseHeaders[k];
658
+ if (agent && res.headers['alt-svc']) {
659
+ agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
333
660
  }
334
661
 
335
- // Alt-Svc discovery
336
- if (agent && flatHeaders['alt-svc']) {
337
- agent.parseAltSvc(hostname, port, flatHeaders['alt-svc']);
338
- }
339
-
340
- var res = new IncomingMessage({
341
- httpVersion: '2.0',
342
- statusCode: statusCode,
343
- headers: flatHeaders
344
- });
345
-
346
662
  clientReq.protocol = 'h2';
347
663
  if (agent) agent.setProtocol(hostname, port, 'h2');
348
-
349
- h2Req.on('data', function (chunk) { res._pushData(chunk); });
350
- h2Req.on('end', function () { res._endData(); });
351
- h2Req.on('error', function (err) { res._abort(err); });
352
-
664
+ res.earlyData = earlyOutcome(h2Req);
353
665
  deliverResponse(res);
354
666
  });
355
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); });
356
671
  h2Req.on('error', function (err) {
357
- onFail(err);
672
+ if (!err.retryable) sess.failed();
673
+ failOver(token, err, function () { tryH2(onFail); }, onFail);
358
674
  });
675
+ clientReq._onCancel(function () { h2Req.destroy(); }); // RST_STREAM (CANCEL)
359
676
 
360
- var body = clientReq._getBody();
361
- if (body) h2Req.write(Buffer.from(body));
362
- h2Req.end();
677
+ bind(token, h2Req);
363
678
 
364
679
  } catch (err) {
365
680
  onFail(err);
@@ -368,53 +683,56 @@ function request(url, options, callback) {
368
683
 
369
684
 
370
685
  // ============================================================
371
- // HTTPS transport (node:https — HTTP/1.1)
686
+ // HTTPS transport (ours — HTTP/1.1 over lemon-tls)
372
687
  // ============================================================
373
688
 
374
689
  function tryHttps(onFail) {
375
690
  if (responded || clientReq.destroyed || clientReq.aborted) return;
376
691
 
377
692
  try {
378
- 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); },
379
697
  hostname: hostname,
380
698
  port: port,
381
699
  path: path,
382
700
  method: method,
383
701
  headers: Object.assign({}, headers),
384
- rejectUnauthorized: options.rejectUnauthorized !== false
385
- };
386
-
387
- 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) {
388
712
  if (responded) return;
389
713
 
390
- if (agent && httpsRes.headers['alt-svc']) {
391
- agent.parseAltSvc(hostname, port, httpsRes.headers['alt-svc']);
714
+ if (agent && res.headers['alt-svc']) {
715
+ agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
392
716
  }
393
717
 
394
- var res = new IncomingMessage({
395
- httpVersion: httpsRes.httpVersion || '1.1',
396
- statusCode: httpsRes.statusCode,
397
- statusMessage: httpsRes.statusMessage || '',
398
- headers: httpsRes.headers || {}
399
- });
400
-
401
718
  clientReq.protocol = 'https';
402
719
  if (agent) agent.setProtocol(hostname, port, 'https');
403
-
404
- httpsRes.on('data', function (chunk) { res._pushData(chunk); });
405
- httpsRes.on('end', function () { res._endData(); });
406
- httpsRes.on('error', function (err) { res._abort(err); });
407
-
720
+ res.earlyData = earlyOutcome(httpsReq);
408
721
  deliverResponse(res);
409
722
  });
410
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.
411
729
  httpsReq.on('error', function (err) {
412
- onFail(err);
730
+ if (!err.retryable) sess.failed();
731
+ failOver(token, err, err.retryable ? function () { tryHttps(onFail); } : null, onFail);
413
732
  });
733
+ clientReq._onCancel(function () { httpsReq.destroy(); }); // the socket goes
414
734
 
415
- var body = clientReq._getBody();
416
- if (body) httpsReq.write(Buffer.from(body));
417
- httpsReq.end();
735
+ bind(token, httpsReq);
418
736
 
419
737
  } catch (err) {
420
738
  onFail(err);
@@ -429,6 +747,15 @@ function request(url, options, callback) {
429
747
  function deliverResponse(res) {
430
748
  if (responded) return;
431
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(); });
432
759
 
433
760
  if (clientReq._timeoutTimer) {
434
761
  clearTimeout(clientReq._timeoutTimer);