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/h3_client.js CHANGED
@@ -14,8 +14,15 @@
14
14
 
15
15
  import dns from 'node:dns';
16
16
  import { DEBUG } from './utils.js';
17
- import { H3Connection } from './h3.js';
17
+ import { H3Connection, H3_ERR_NAME } from './h3.js';
18
18
  import { createQuicClientSocket } from './quic_socket.js';
19
+ import { parseVersionList } from './quic_version.js';
20
+ import { checkConnectionOptions, CONNECTION_OPTIONS } from './quic_connection.js';
21
+ import { trailerFields, formatPriority } from './http_common.js';
22
+
23
+ // What the body takes before write() says wait — written while connecting,
24
+ // or not yet sent on the stream.
25
+ var BODY_HIGH_WATER = 64 * 1024;
19
26
 
20
27
 
21
28
  // ============================================================
@@ -34,6 +41,18 @@ function request(options, callback) {
34
41
  }
35
42
 
36
43
  var hostname = options.hostname || options.host || 'localhost';
44
+ // Certificate verification options, forwarded to the QUIC/TLS layer.
45
+ // Defaults to verifying (node:tls semantics); without this passthrough the
46
+ // option dies here and H3 connections can never opt out — which makes the
47
+ // one transport this library exists for the only one that can't reach a
48
+ // self-signed or private-CA server (H2/H1.1 fallbacks honour the option).
49
+ var rejectUnauthorized = options.rejectUnauthorized !== false;
50
+ // Checked now, as https.request checks its options: thrown here, not from
51
+ // inside the socket's callback later.
52
+ parseVersionList(options.quicVersions, 'quicVersions');
53
+ // The connection options it sets, checked now (thrown here, as above).
54
+ (function () { var o = {}; for (var k in CONNECTION_OPTIONS) if (k !== 'carefulResume' && options[k] != null) o[k] = options[k]; checkConnectionOptions(o, 'request'); })();
55
+ var ca = options.ca || null;
37
56
  var port = options.port || 443;
38
57
  var path = options.path || '/';
39
58
  var method = (options.method || 'GET').toUpperCase();
@@ -58,6 +77,28 @@ function request(options, callback) {
58
77
  var udpSocket = existingConn ? existingConn.udpSocket : null;
59
78
  var streamId = null; // assigned when connection is ready
60
79
  var connected = existingConn ? true : false;
80
+ var requestSent = false; // HEADERS went out on streamId
81
+ var readyDone = false; // the stream exists and the request is this one's to send (ready)
82
+ // 0-RTT (RFC 9001 §4.6, RFC 8470): with earlyData and a saved session, a
83
+ // request that may be replayed safely — GET, HEAD, OPTIONS — goes before
84
+ // the handshake ends. res.earlyData: 'accepted' (it went early and was
85
+ // taken), 'rejected' (refused — by the server's TLS, or by a 425 — and
86
+ // sent again after the handshake), null (not tried).
87
+ // Decided when a connection is made (startConnection): the session is
88
+ // read there — the unified client's is taken from its agent on that read,
89
+ // and a request on a connection already open must not use one up.
90
+ var earlyWanted = false;
91
+ var earlyTried = false; // set up on the early key (onEarly)
92
+ var earlyResult = null;
93
+ var earlyBody = null; // what went early, to send again if refused
94
+ var handshaken = false; // onConnect has come
95
+ var retry425 = false; // the server answered the early request 425: again, after the handshake
96
+ var after425 = null; // to run at onConnect, when the 425 came first
97
+ var streaming = false; // HEADERS sent before the body ended: DATA follows as written
98
+ var reqBodyBytes = 0, needDrain = false, drainWaiting = false;
99
+ var reqTrailers = null;
100
+ var finished = false; // the response ended or failed
101
+ var cancelled = false; // destroy() was called
61
102
 
62
103
  var clientReq = {
63
104
  on: function (event, fn) {
@@ -65,22 +106,70 @@ function request(options, callback) {
65
106
  return clientReq;
66
107
  },
67
108
 
109
+ // The body, as node:http's: held while connecting (write() false past
110
+ // BODY_HIGH_WATER, then 'drain'), then streamed — HEADERS on the first
111
+ // write once the stream exists, DATA as written, with the stream's
112
+ // backpressure.
68
113
  write: function (chunk) {
114
+ if (finished || cancelled || reqFinished) return false;
69
115
  if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
116
+ if (!chunk || chunk.byteLength === 0) return !needDrain;
117
+ if (!streaming && readyDone && !requestSent) startStreaming();
118
+ if (streaming) return streamOut(chunk);
70
119
  reqBody.push(chunk);
120
+ reqBodyBytes += chunk.byteLength;
121
+ if (reqBodyBytes >= BODY_HIGH_WATER) { needDrain = true; return false; }
71
122
  return true;
72
123
  },
73
124
 
74
- end: function (chunk) {
75
- if (chunk) clientReq.write(chunk);
125
+ // RFC 9218: the `priority` header while the request has not gone, a
126
+ // PRIORITY_UPDATE on the control stream after.
127
+ setPriority: function (p) {
128
+ if (requestSent) { if (h3 && !finished && !cancelled) h3.sendPriorityUpdate(streamId, p); return; }
129
+ for (var k in headers) if (k.toLowerCase() === 'priority') delete headers[k];
130
+ var v = formatPriority(p);
131
+ if (v) headers.priority = v;
132
+ },
133
+
134
+ // `trailers` (node:http's addTrailers, through the unified client): a
135
+ // HEADERS frame after the body, ending the stream (RFC 9114 §4.1).
136
+ end: function (chunk, trailers) {
137
+ if (reqFinished) return clientReq;
138
+ if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
139
+ reqTrailers = trailerFields(trailers);
140
+ if (streaming) {
141
+ reqFinished = true;
142
+ if (chunk && chunk.byteLength) h3.sendBody(streamId, chunk, !reqTrailers);
143
+ else if (!reqTrailers) h3.sendBody(streamId, null, true);
144
+ if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
145
+ return clientReq;
146
+ }
147
+ if (chunk && chunk.byteLength) { reqBody.push(chunk); reqBodyBytes += chunk.byteLength; }
76
148
  reqFinished = true;
77
- if (connected) sendRequest();
149
+ if (readyDone) sendRequest();
78
150
  return clientReq;
79
151
  },
80
152
 
153
+ // Cancel this request. Its stream is what ends (RFC 9114 §4.1.1:
154
+ // STOP_SENDING and RESET_STREAM with H3_REQUEST_CANCELLED) — not the
155
+ // connection, which may carry other requests (a pooled one does). This
156
+ // closed the whole connection, every other request on it included.
157
+ // A connection that is this request's alone is closed with it.
81
158
  destroy: function () {
82
- if (quic) quic.close(0, 'client destroy');
83
- if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
159
+ if (cancelled || finished) return;
160
+ cancelled = true;
161
+ var conn = clientReq._connection || existingConn;
162
+ if (conn && conn._streamHandlers && streamId !== null) delete conn._streamHandlers[streamId];
163
+ // Still connecting: closed in onConnect (or ends by itself if the
164
+ // handshake fails) — closing the socket under a handshake in progress
165
+ // would leave its timers sending into a closed socket.
166
+ if (autoClose) { if (quic) closeConnection(); return; }
167
+ // Sent or not: its stream ID is taken, and a later request's higher
168
+ // one opens it at the server (RFC 9000 §3.2) — where nothing would
169
+ // ever end it, a stream of the connection's limit lost for good. So it
170
+ // is cancelled either way; never written, its RESET_STREAM carries a
171
+ // final size of 0 (sendRequest() will not run).
172
+ if (h3 && streamId !== null) h3.abortStream(streamId, 0x10c); // H3_REQUEST_CANCELLED
84
173
  }
85
174
  };
86
175
 
@@ -88,14 +177,26 @@ function request(options, callback) {
88
177
  var clientRes = {
89
178
  statusCode: 0,
90
179
  headers: {},
180
+ trailers: {},
181
+ rawTrailers: [],
91
182
  _listeners: {},
92
183
  _headersReceived: false,
93
184
 
94
185
  on: function (event, fn) {
95
186
  (clientRes._listeners[event] = clientRes._listeners[event] || []).push(fn);
96
187
  return clientRes;
97
- }
188
+ },
189
+
190
+ // The body's flow-control credit, returned as the layer above reads it
191
+ // rather than as it arrives (h3's holdBody) — as the server does with a
192
+ // request body. Credited on delivery, a response nobody read yet was
193
+ // taken in as fast as the server could send it: a slow consumer of a
194
+ // large download held all of it in memory, and the server never slowed.
195
+ _holdBody: function () { if (respConn && respConn.h3) respConn.h3.holdBody(streamId); },
196
+ _creditBody: function (n) { if (respConn && respConn.h3) respConn.h3.creditBody(streamId, n); },
197
+ _releaseBody: function () { if (respConn && respConn.h3) respConn.h3.releaseBody(streamId); }
98
198
  };
199
+ var respConn = null; // the connection the response comes on (setupResponseHandlers)
99
200
 
100
201
  function emit(obj, event, data) {
101
202
  var listeners = (obj._listeners || obj)[event] || [];
@@ -115,7 +216,7 @@ function request(options, callback) {
115
216
  streamId = allocateStreamId(existingConn);
116
217
  installSharedHandlers(existingConn);
117
218
  setupResponseHandlers(existingConn);
118
- if (reqFinished) sendRequest();
219
+ ready(existingConn);
119
220
  } else {
120
221
  resolveAndConnect(hostname, port);
121
222
  }
@@ -140,18 +241,122 @@ function request(options, callback) {
140
241
 
141
242
 
142
243
  function startConnection(remoteIp, remotePort) {
244
+ if (cancelled) return;
245
+ var sessionNow = options.session || null;
246
+ earlyWanted = options.earlyData === true && !!sessionNow &&
247
+ (method === 'GET' || method === 'HEAD' || method === 'OPTIONS');
143
248
  udpSocket = createQuicClientSocket({
144
249
  remoteIp: remoteIp,
145
250
  remotePort: remotePort,
146
- hostname: hostname,
251
+ // SNI and the name the certificate is checked for: tls.connect's
252
+ // servername where given.
253
+ hostname: options.servername || hostname,
254
+ rejectUnauthorized: rejectUnauthorized,
255
+ ca: ca,
256
+ // A client certificate, for a server that asks (https.request's names).
257
+ key: options.key || null, cert: options.cert || null, passphrase: options.passphrase,
258
+ // A saved session to resume with, and the request's 'session' event for
259
+ // each the server sends — as https.request does (its socket's
260
+ // 'session'). Only the request that opened the connection has one: a
261
+ // request on a connection already open sees no handshake.
262
+ session: sessionNow,
263
+ // The server's stapled OCSP response, as the request's 'OCSPResponse'
264
+ // (tls.connect's requestOCSP) — on the request that opened the connection.
265
+ requestOCSP: options.requestOCSP === true,
266
+ // Encrypted Client Hello: the server's ECHConfigList, or GREASE ECH.
267
+ ech: options.ech || null,
268
+ echGrease: options.echGrease === true,
269
+ // The key-exchange groups to offer (tls.connect's ecdhCurve).
270
+ ecdhCurve: options.ecdhCurve,
271
+ // The signature schemes to offer (tls.connect's sigalgs).
272
+ sigalgs: options.sigalgs,
273
+ quicVersions: options.quicVersions,
274
+ // This client's connection options (quic_connection's CONNECTION_OPTIONS):
275
+ // Startup's cap on what it sends (an upload's), path MTU discovery, the
276
+ // largest UDP payload, the RTT assumed before a sample, the reordering
277
+ // a packet is allowed, the initial window.
278
+ startupCap: options.startupCap,
279
+ pmtuDiscovery: options.pmtuDiscovery,
280
+ maxUdpPayloadSize: options.maxUdpPayloadSize,
281
+ initialRtt: options.initialRtt,
282
+ reorderWindow: options.reorderWindow,
283
+ initialCongestionWindow: options.initialCongestionWindow,
284
+ // A NEW_TOKEN token for the Initial (the agent keeps them), and the
285
+ // request's 'newToken' event for the one the server gives for next time.
286
+ token: options.token || null,
287
+ earlyData: earlyWanted,
288
+ onEarly: function (q, s) {
289
+ if (cancelled) return;
290
+ earlyTried = true;
291
+ earlyResult = 'accepted'; // until the server says otherwise
292
+ setUp(q, s);
293
+ },
294
+ onSocket: function (q) {
295
+ q.on('session', function (buf) { emit(reqListeners, 'session', buf); });
296
+ q.on('new_token', function (buf) { emit(reqListeners, 'newToken', buf); });
297
+ q.on('OCSPResponse', function (buf) { emit(reqListeners, 'OCSPResponse', buf); });
298
+ },
147
299
 
148
300
  onError: function (err) {
301
+ if (cancelled) return;
149
302
  emit(reqListeners, 'error', err);
150
303
  },
151
304
 
152
305
  onConnect: function (q, s) {
306
+ // Sent early: taken, nothing more to do — or refused, and the
307
+ // connection has forgotten it: HTTP/3 starts over on it, and the
308
+ // request goes again, in 1-RTT.
309
+ if (earlyTried) {
310
+ handshaken = true;
311
+ if (cancelled) { closeConnection(); return; }
312
+ if (after425) { var a425 = after425; after425 = null; a425(); return; }
313
+ if (q.earlyDataStatus !== 'rejected') return;
314
+ earlyResult = 'rejected';
315
+ redoAfterEarly();
316
+ return;
317
+ }
318
+ setUp(q, s);
319
+ }
320
+ // onClose: the helper closes the UDP socket; nothing extra needed here.
321
+ });
322
+ }
323
+
324
+ // What was sent early, refused: this request's stream and the HTTP/3
325
+ // layer it went through are dropped, and both made again.
326
+ function redoAfterEarly() {
327
+ var old = respConn;
328
+ if (old && old._streamHandlers) delete old._streamHandlers[streamId];
329
+ if (h3 && h3.detach) h3.detach();
330
+ requestSent = false; readyDone = false; streaming = false;
331
+ if (earlyBody) { reqBody = earlyBody.body; reqTrailers = earlyBody.trailers; reqBodyBytes = 0; }
332
+ setUp(quic, udpSocket);
333
+ }
334
+
335
+ // The early request answered 425: on a new stream of the same connection,
336
+ // once the handshake is done (now, or at onConnect).
337
+ function againAfter425() {
338
+ function go() {
339
+ if (cancelled || finished) return;
340
+ var conn = respConn;
341
+ requestSent = false; readyDone = false; streaming = false;
342
+ if (earlyBody) { reqBody = earlyBody.body; reqTrailers = earlyBody.trailers; }
343
+ earlyBody = null;
344
+ retry425 = false;
345
+ streamId = allocateStreamId(conn);
346
+ setupResponseHandlers(conn);
347
+ ready(conn);
348
+ }
349
+ if (handshaken) go(); else after425 = go;
350
+ }
351
+
352
+ function setUp(q, s) {
353
+ (function () {
153
354
  quic = q;
154
355
  udpSocket = s;
356
+ // Cancelled while connecting: the connection was for this request
357
+ // (an owned one, or a pool lead whose followers have been released),
358
+ // and nobody else holds it.
359
+ if (cancelled) { closeConnection(); return; }
155
360
  if (DEBUG) console.log('[client] QUIC connected');
156
361
 
157
362
  h3 = new H3Connection({ quicConnection: quic, isServer: false });
@@ -169,12 +374,8 @@ function request(options, callback) {
169
374
  clientReq._connection = conn;
170
375
 
171
376
  setupResponseHandlers(conn);
172
-
173
- // Send request if body is ready
174
- if (reqFinished) sendRequest();
175
- }
176
- // onClose: the helper closes the UDP socket; nothing extra needed here.
177
- });
377
+ ready(conn);
378
+ })();
178
379
  }
179
380
 
180
381
 
@@ -210,12 +411,80 @@ function request(options, callback) {
210
411
  var h = conn._streamHandlers[sid];
211
412
  if (h) h.onBody(data);
212
413
  });
414
+ h3c.on('http_trailers', function (sid, trailers) {
415
+ var h = conn._streamHandlers[sid];
416
+ if (h && h.onTrailers) h.onTrailers(trailers);
417
+ });
213
418
  h3c.on('http_end', function (sid) {
214
419
  var h = conn._streamHandlers[sid];
215
420
  if (!h) return;
216
421
  delete conn._streamHandlers[sid]; // free per-stream state before dispatch
217
422
  h.onEnd();
218
423
  });
424
+ // The response stream was ended over what the server sent on it (an
425
+ // oversized HEADERS, a stream with no response): the request fails,
426
+ // rather than waiting for an end that will not come.
427
+ h3c.on('stream_error', function (sid, code, reason) {
428
+ var h = conn._streamHandlers[sid];
429
+ if (!h) return;
430
+ delete conn._streamHandlers[sid];
431
+ if (h.onError) h.onError(code, reason);
432
+ });
433
+ // The server reset the request stream (RESET_STREAM): the response, or
434
+ // what is left of it, will not come. Nothing listened, and a response
435
+ // cut off this way never ended nor failed — it hung.
436
+ h3c.on('stream_reset', function (sid, code) {
437
+ var h = conn._streamHandlers[sid];
438
+ if (!h) return;
439
+ delete conn._streamHandlers[sid];
440
+ if (h.onReset) h.onReset(code);
441
+ });
442
+ // The server will not read the rest of the request (STOP_SENDING; QUIC
443
+ // has reset our sending part in answer). Not heard before: a request
444
+ // the server refused this way went on "writing" into nothing, and was
445
+ // never told.
446
+ h3c.on('stop_sending', function (sid, code) {
447
+ var h = conn._streamHandlers[sid];
448
+ if (h && h.onStopSending) h.onStopSending(code);
449
+ });
450
+
451
+ // GOAWAY (RFC 9114 §5.2): requests on streams from its id on were not,
452
+ // and will not be, processed — retryable, as HTTP/2's (h2_client). And
453
+ // the connection's end ends every request still on it. Neither was
454
+ // heard: a request sent as the server's GOAWAY was on its way — the
455
+ // server rejected it and closed, its RESET_STREAM behind the close —
456
+ // waited for a response forever.
457
+ function failAll(fromId, make) {
458
+ for (var sid in conn._streamHandlers) {
459
+ if (Number(sid) < fromId) continue;
460
+ var h = conn._streamHandlers[sid];
461
+ delete conn._streamHandlers[sid];
462
+ if (h.onGone) h.onGone(make(Number(sid)));
463
+ }
464
+ }
465
+ conn._goawayId = Infinity;
466
+ h3c.on('goaway', function (id) {
467
+ var goawayId = conn._goawayId = Math.min(conn._goawayId, Number(id));
468
+ // For a pool: no new requests here (as an HTTP/2 session's goingAway).
469
+ // The GOAWAY can come before the response that puts the connection in
470
+ // the pool, so the pool asks rather than waits for the event.
471
+ conn.goingAway = true;
472
+ failAll(goawayId, function () {
473
+ var e = new Error('HTTP/3 request not processed before GOAWAY');
474
+ e.code = 'ERR_HTTP3_GOAWAY_UNPROCESSED';
475
+ e.retryable = true;
476
+ return e;
477
+ });
478
+ });
479
+ conn.quic.on('close', function () {
480
+ failAll(0, function (sid) {
481
+ var e = new Error('HTTP/3 connection closed before the response ended');
482
+ e.code = 'ERR_HTTP3_CONNECTION_CLOSED';
483
+ // Past a GOAWAY's id the server said it did not process it.
484
+ e.retryable = sid >= conn._goawayId;
485
+ return e;
486
+ });
487
+ });
219
488
  }
220
489
 
221
490
 
@@ -225,11 +494,37 @@ function request(options, callback) {
225
494
  * routing is now by map key, so no per-listener streamId check is needed.
226
495
  */
227
496
  function setupResponseHandlers(conn) {
497
+ respConn = conn;
498
+ // A connection the server is done with — GOAWAY'd below this stream, or
499
+ // gone — takes no request: it fails now, as it would have on the wire.
500
+ // On the next tick: the caller has not attached its listeners yet.
501
+ var state = conn.quic ? conn.quic.state : 'closed';
502
+ if (streamId >= conn._goawayId || state === 'closed' || state === 'closing' || state === 'draining') {
503
+ var gone = new Error(streamId >= conn._goawayId ? 'HTTP/3 request not processed before GOAWAY' : 'HTTP/3 connection closed before the response ended');
504
+ gone.code = streamId >= conn._goawayId ? 'ERR_HTTP3_GOAWAY_UNPROCESSED' : 'ERR_HTTP3_CONNECTION_CLOSED';
505
+ gone.retryable = true;
506
+ process.nextTick(function () { fail(gone); });
507
+ return;
508
+ }
228
509
  conn._streamHandlers[streamId] = {
229
510
  onHeaders: function (hdrs) {
511
+ // 1xx (103 Early Hints): informational — the response follows on the
512
+ // same stream. node:http's 'information', not the response: taken as
513
+ // it, the request got status 103 and the real head was lost.
514
+ var st1 = parseInt(hdrs[':status'], 10);
515
+ if (st1 >= 100 && st1 < 200) {
516
+ var ih = {}, raw = [];
517
+ for (var hn in hdrs) { if (hn[0] !== ':') { ih[hn] = hdrs[hn]; raw.push(hn, String(hdrs[hn])); } }
518
+ emit(reqListeners, 'information', { statusCode: st1, statusMessage: '', httpVersion: '3.0', httpVersionMajor: 3, httpVersionMinor: 0, headers: ih, rawHeaders: raw });
519
+ return;
520
+ }
521
+ // 425 Too Early to what went in 0-RTT (RFC 8470 §5.2): not the
522
+ // response — the request goes again once the handshake is done.
523
+ if (st1 === 425 && earlyBody && !retry425) { retry425 = true; earlyResult = 'rejected'; return; }
230
524
  clientRes.headers = hdrs;
231
525
  clientRes.statusCode = parseInt(hdrs[':status']) || 0;
232
526
  clientRes._headersReceived = true;
527
+ clientRes.earlyData = earlyTried ? earlyResult : null;
233
528
 
234
529
  // Fire callback with response (once)
235
530
  if (typeof callback === 'function') {
@@ -240,62 +535,195 @@ function request(options, callback) {
240
535
  },
241
536
 
242
537
  onBody: function (data) {
538
+ if (retry425) return;
243
539
  emit(clientRes, 'data', data);
244
540
  },
245
541
 
542
+ // The response's trailers: res.trailers / res.rawTrailers, as node:http
543
+ // has them by 'end' (gRPC's grpc-status).
544
+ onTrailers: function (trailers) {
545
+ clientRes.trailers = trailers;
546
+ clientRes.rawTrailers = [];
547
+ for (var tn in trailers) clientRes.rawTrailers.push(tn, String(trailers[tn]));
548
+ },
549
+
550
+ onError: function (code, reason) {
551
+ var err = new Error('HTTP/3 stream error: ' + reason);
552
+ err.code = 'ERR_HTTP3_STREAM_ERROR';
553
+ err.errorCode = code;
554
+ fail(err);
555
+ },
556
+
557
+ onReset: function (code) {
558
+ var name = H3_ERR_NAME[code] || ('0x' + Number(code).toString(16));
559
+ var err = new Error('HTTP/3 stream reset by the server: ' + name);
560
+ err.code = 'ERR_HTTP3_STREAM_RESET';
561
+ err.errorCode = code;
562
+ // H3_REQUEST_REJECTED: the server did not process it (RFC 9114 §4.1.1)
563
+ // — safe to retry, as HTTP/2's REFUSED_STREAM. Any other code is the
564
+ // server's answer, not a failure to reach it: not sent again over TCP.
565
+ err.retryable = (code === 0x10b);
566
+ if (!err.retryable) err.requestError = true;
567
+ fail(err);
568
+ },
569
+
570
+ // H3_NO_ERROR: the server answers without the rest of the body (RFC
571
+ // 9114 §4.1.2) — the response comes, and is not to be discarded. Any
572
+ // other code before a response: the request is refused or cancelled,
573
+ // as by a reset (H3_REQUEST_REJECTED retryable).
574
+ onStopSending: function (code) {
575
+ if (code === 0x100 || clientRes._headersReceived) return;
576
+ var h = conn._streamHandlers[streamId];
577
+ if (h) delete conn._streamHandlers[streamId];
578
+ var name = H3_ERR_NAME[code] || ('0x' + Number(code).toString(16));
579
+ var err = new Error('HTTP/3 request stopped by the server: ' + name);
580
+ err.code = 'ERR_HTTP3_STREAM_RESET';
581
+ err.errorCode = code;
582
+ err.retryable = (code === 0x10b);
583
+ if (!err.retryable) err.requestError = true;
584
+ fail(err);
585
+ },
586
+
587
+ // The connection gone (or past GOAWAY) under it. Sent, not processed
588
+ // per GOAWAY, and not idempotent: the server may have acted on it — not
589
+ // sent again over TCP (RFC 9110 §9.2.2).
590
+ onGone: function (err) {
591
+ if (!err.retryable && requestSent && !({ GET: 1, HEAD: 1, OPTIONS: 1, TRACE: 1, PUT: 1, DELETE: 1 })[method]) err.requestError = true;
592
+ fail(err);
593
+ },
594
+
246
595
  onEnd: function () {
596
+ if (retry425 && !clientRes._headersReceived) { againAfter425(); return; }
597
+ finished = true;
598
+ clientRes._releaseBody(); // all of it has arrived: nothing more to wait for
247
599
  emit(clientRes, 'end');
248
-
249
- // Auto-close only if we own the connection (see `autoClose` above).
250
- if (autoClose) {
251
- setTimeout(function () {
252
- if (quic) quic.close(0, 'done');
253
- if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
254
- }, 100);
255
- }
600
+ closeIfOwned();
256
601
  }
257
602
  };
258
603
  }
259
604
 
605
+ // The request failed. Before the response began, the request reports it
606
+ // ('error' — a fallback may still be tried); after, the response does —
607
+ // the request's error was reported to a caller that had moved on to the
608
+ // response, and the response hung.
609
+ function fail(err) {
610
+ if (finished) return;
611
+ finished = true;
612
+ clientRes._releaseBody();
613
+ if (clientRes._headersReceived) emit(clientRes, 'error', err);
614
+ else emit(reqListeners, 'error', err);
615
+ closeIfOwned();
616
+ }
260
617
 
261
- function sendRequest() {
262
- if (!h3 || !connected) return;
618
+ // Auto-close only if we own the connection (see `autoClose` above) — after
619
+ // the response ends, or fails.
620
+ // An answer to early data can come before the handshake is confirmed,
621
+ // and with it the server's session ticket: the close waits for that, or
622
+ // the next visit would have no ticket to resume (and send early) with.
623
+ function closeIfOwned() {
624
+ if (!autoClose) return;
625
+ if (earlyTried && quic && quic.whenConfirmed) quic.whenConfirmed(function () { setTimeout(closeConnection, 100); });
626
+ else setTimeout(closeConnection, 100);
627
+ }
263
628
 
264
- if (DEBUG) console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
629
+ // RFC 9114 §5.2: closed with an HTTP/3 code in the application space.
630
+ // H3_NO_ERROR. The UDP socket closes when the connection does — its
631
+ // closing period over (quic_socket, on 'close') — not here: closed at once,
632
+ // the CONNECTION_CLOSE just queued on it was cancelled with it, and the
633
+ // server never learned the connection was over. A request with agent:
634
+ // false left the server's connection (and a response it was still
635
+ // writing) up until its idle timeout.
636
+ function closeConnection() {
637
+ if (quic) quic.close(0x0100, 'done', true);
638
+ else if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
639
+ }
265
640
 
266
- // MUST send control streams (SETTINGS) before any request
267
- h3.sendControlStreams();
268
641
 
269
- // Build H3 request headers
642
+ // The stream exists. options.onReady (the unified client) may still turn
643
+ // the request down — another protocol was ready first and carries it:
644
+ // nothing goes out, and 'superseded' says so (the connection stays, for
645
+ // the pool). Otherwise: sent whole if the body is complete, streamed if
646
+ // some of it was written, or on the first write or end().
647
+ function ready(conn) {
648
+ if (typeof options.onReady === 'function' && !options.onReady()) {
649
+ finished = true;
650
+ if (conn && conn._streamHandlers && streamId !== null) delete conn._streamHandlers[streamId];
651
+ // Its stream ID, taken and never used: cancelled, as by destroy()
652
+ // (an unused ID below a later request's is opened by that request).
653
+ if (!autoClose && conn && conn.h3 && streamId !== null) { try { conn.h3.abortStream(streamId, 0x10c); } catch (e) {} }
654
+ // Next tick: on a pooled connection this runs inside request(), before
655
+ // the caller has its listeners on.
656
+ process.nextTick(function () { emit(reqListeners, 'superseded'); });
657
+ if (autoClose && quic) closeConnection();
658
+ return;
659
+ }
660
+ readyDone = true;
661
+ if (reqFinished) sendRequest();
662
+ else if (reqBody.length) startStreaming();
663
+ }
664
+
665
+ function requestHeaders() {
270
666
  var h3Headers = {
271
667
  ':method': method,
272
668
  ':scheme': 'https',
273
669
  ':authority': hostname + (port !== 443 ? ':' + port : ''),
274
670
  ':path': path
275
671
  };
276
-
277
- // Add user headers
278
- for (var name in headers) {
279
- h3Headers[name.toLowerCase()] = String(headers[name]);
280
- }
281
-
282
- // Default headers
672
+ for (var name in headers) h3Headers[name.toLowerCase()] = String(headers[name]);
283
673
  if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
284
674
  if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
285
675
  if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
676
+ return h3Headers;
677
+ }
286
678
 
287
- // Send body if any
288
- if (reqBody.length > 0) {
289
- // Send headers without FIN, then body chunks
290
- h3.sendHeaders(streamId, h3Headers, false);
291
- for (var i = 0; i < reqBody.length; i++) {
292
- var isLast = (i === reqBody.length - 1);
293
- h3.sendBody(streamId, reqBody[i], isLast);
294
- }
295
- } else {
296
- // No body — send headers with FIN directly
297
- h3.sendHeaders(streamId, h3Headers, true);
298
- }
679
+ // The request whole: ended before it could go — headers, the body, the
680
+ // trailers; FIN on the last of them.
681
+ function sendRequest() {
682
+ if (!h3 || !connected || cancelled || finished || requestSent) return;
683
+ requestSent = true;
684
+ if (DEBUG) console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
685
+ h3.sendControlStreams(); // SETTINGS before any request
686
+ var body = reqBody; reqBody = []; reqBodyBytes = 0;
687
+ // Early: kept, to go again if the server refuses it.
688
+ if (earlyTried && !handshaken) earlyBody = { body: body.slice(), trailers: reqTrailers };
689
+ if (body.length === 0 && !reqTrailers) { h3.sendHeaders(streamId, requestHeaders(), true); return; }
690
+ h3.sendHeaders(streamId, requestHeaders(), false);
691
+ for (var i = 0; i < body.length; i++) h3.sendBody(streamId, body[i], i === body.length - 1 && !reqTrailers);
692
+ if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
693
+ }
694
+
695
+ // The body streamed: HEADERS now, without FIN, then what was held.
696
+ function startStreaming() {
697
+ if (!h3 || cancelled || finished || requestSent) return;
698
+ requestSent = true;
699
+ streaming = true;
700
+ if (DEBUG) console.log('[client] streaming request method=' + method + ' path=' + path + ' stream=' + streamId);
701
+ h3.sendControlStreams();
702
+ h3.sendHeaders(streamId, requestHeaders(), false);
703
+ var held = reqBody; reqBody = []; reqBodyBytes = 0;
704
+ var more = true;
705
+ for (var i = 0; i < held.length; i++) more = streamOut(held[i]);
706
+ if (more && needDrain) { needDrain = false; emit(reqListeners, 'drain'); }
707
+ }
708
+
709
+ // One piece of a streamed body. Returns whether more may be written now:
710
+ // false while more than BODY_HIGH_WATER of the stream is not yet sent,
711
+ // and 'drain' when it is (the stream's send side, whenStreamSent).
712
+ function streamOut(chunk) {
713
+ h3.sendBody(streamId, chunk, false);
714
+ if (drainWaiting) { needDrain = true; return false; }
715
+ var later = false, over = true;
716
+ quic.whenStreamSent(streamId, BODY_HIGH_WATER, function () {
717
+ over = false;
718
+ if (!later) return;
719
+ drainWaiting = false;
720
+ if (needDrain) { needDrain = false; emit(reqListeners, 'drain'); }
721
+ });
722
+ later = true;
723
+ if (!over) return true;
724
+ drainWaiting = true;
725
+ needDrain = true;
726
+ return false;
299
727
  }
300
728
 
301
729