Skip to content

Commit 33e84b7

Browse files
committed
quic: use http3Binding to drop header & priority stream logic from QUIC
Signed-off-by: Tim Perry <pimterry@gmail.com>
1 parent 58eae6b commit 33e84b7

21 files changed

Lines changed: 695 additions & 534 deletions

‎lib/internal/quic/http3.js‎

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
ArrayIsArray,
55
ArrayPrototypePush,
6+
ObjectHasOwn,
67
Symbol,
78
SymbolAsyncIterator,
89
} = primordials;
@@ -40,6 +41,7 @@ const {
4041

4142
const {
4243
setHttp3Callbacks,
44+
createHttp3Handle,
4345

4446
QUIC_STREAM_HEADERS_KIND_INITIAL: kHeadersKindInitial,
4547
QUIC_STREAM_HEADERS_KIND_HINTS: kHeadersKindHints,
@@ -65,8 +67,10 @@ const {
6567
} = require('internal/quic/diagnostics');
6668

6769
const {
70+
validateBoolean,
6871
validateFunction,
6972
validateObject,
73+
validateOneOf,
7074
} = require('internal/validators');
7175

7276
const {
@@ -133,14 +137,29 @@ function parseHeaderPairs(pairs) {
133137
* will be sent after these headers.
134138
*/
135139

140+
const kGetHttp3Handle = Symbol('kGetHttp3Handle');
141+
const kSubmitInitialHeaders = Symbol('kSubmitInitialHeaders');
142+
143+
function priorityFieldValue(level, incremental) {
144+
const urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3;
145+
if (urgency === 3 && !incremental) return undefined;
146+
return incremental ? `u=${urgency}, i` : `u=${urgency}`;
147+
}
148+
136149
class Http3Stream {
137150
#stream;
138151
#session;
152+
#h3handle;
139153
#headers = undefined;
140154
#onheaders = undefined;
141155
#oninfo = undefined;
142156
#ontrailers = undefined;
143157
#onwanttrailers = undefined;
158+
// Client-side stored priority (the value this side requested). The server
159+
// side reads the peer's requested priority from nghttp3 instead.
160+
#priority = { __proto__: null, level: 'default', incremental: false };
161+
// Have we submitted an initial header block (request/response) to nghttp3?
162+
#headersSubmitted = false;
144163

145164
/**
146165
* @param {QuicStream} stream the underlying QUIC stream
@@ -153,6 +172,7 @@ class Http3Stream {
153172
}
154173
this.#stream = stream;
155174
this.#session = session;
175+
this.#h3handle = session[kGetHttp3Handle]();
156176
const handle = stream[kStreamHandle];
157177
if (handle !== undefined) {
158178
handle[kApplicationOwner] = this;
@@ -246,9 +266,57 @@ class Http3Stream {
246266
// True when the stream's data was received as 0-RTT early data.
247267
get early() { return this.#stream.early; }
248268

249-
get priority() { return this.#stream.priority; }
269+
get #isServer() {
270+
return getQuicSessionState(this.#session.session).isServer;
271+
}
250272

251-
setPriority(options) { return this.#stream.setPriority(options); }
273+
get #knownToNgHttp3() {
274+
return this.#isServer || this.#headersSubmitted;
275+
}
276+
277+
// The stream's priority. On a client this is the value we requested; on a
278+
// server it is the peer's requested priority read from nghttp3.
279+
get priority() {
280+
const stream = this.#stream;
281+
if (stream.destroyed || this.#h3handle === undefined) return null;
282+
if (!this.#isServer) {
283+
return { level: this.#priority.level,
284+
incremental: this.#priority.incremental };
285+
}
286+
const packed = this.#h3handle.getPriority(stream[kStreamHandle]);
287+
if (packed === undefined) return null;
288+
const urgency = packed >> 1;
289+
const incremental = !!(packed & 1);
290+
const level = urgency < 3 ? 'high' : urgency > 3 ? 'low' : 'default';
291+
return { level, incremental };
292+
}
293+
294+
setPriority(options = kEmptyObject) {
295+
const stream = this.#stream;
296+
if (stream.destroyed) return;
297+
validateObject(options, 'options');
298+
const { level = 'default', incremental = false } = options;
299+
validateOneOf(level, 'options.level', ['default', 'low', 'high']);
300+
validateBoolean(incremental, 'options.incremental');
301+
this.#priority = { __proto__: null, level, incremental };
302+
303+
// Before the stream is known to nghttp3 (client, pre-submit) the requested
304+
// priority is carried in the initial header block by sendHeaders;
305+
// afterwards a change is signaled with a PRIORITY_UPDATE frame.
306+
if (this.#knownToNgHttp3 && this.#h3handle !== undefined) {
307+
const urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3;
308+
this.#h3handle.setPriority(
309+
stream[kStreamHandle], (urgency << 1) | (incremental ? 1 : 0));
310+
}
311+
}
312+
313+
[kSubmitInitialHeaders](headerString, flags) {
314+
const stream = this.#stream;
315+
if (stream.destroyed || this.#h3handle === undefined) return false;
316+
this.#headersSubmitted = true;
317+
return this.#h3handle.sendHeaders(
318+
stream[kStreamHandle], headerString, flags);
319+
}
252320

253321
#updateHeaderInterest() {
254322
getQuicStreamState(this.#stream).wantsHeaders =
@@ -308,14 +376,29 @@ class Http3Stream {
308376
*/
309377
sendHeaders(headers, options = kEmptyObject) {
310378
const stream = this.#stream;
311-
if (stream.destroyed) return false;
379+
if (stream.destroyed || this.#h3handle === undefined) return false;
312380
validateObject(headers, 'headers');
313381
const { terminal = false } = options;
382+
383+
// A client request carries its requested priority as a priority header
384+
// when set - server responses signal via setPriority instead.
385+
let toSend = headers;
386+
if (!this.#isServer) {
387+
const pri = priorityFieldValue(
388+
this.#priority.level, this.#priority.incremental);
389+
if (pri !== undefined && !ObjectHasOwn(headers, 'priority')) {
390+
toSend = { __proto__: null, ...headers, priority: pri };
391+
}
392+
}
393+
314394
const headerString = buildNgHeaderString(
315-
headers, assertValidPseudoHeader, true /* strictSingleValueFields */);
395+
toSend, assertValidPseudoHeader, true /* strictSingleValueFields */);
316396
const flags = terminal ? kHeadersFlagsTerminal : kHeadersFlagsNone;
317-
return stream[kStreamHandle].sendHeaders(
318-
kHeadersKindInitial, headerString, flags);
397+
// The stream now exists in nghttp3; later priority changes use
398+
// PRIORITY_UPDATE rather than the header block.
399+
this.#headersSubmitted = true;
400+
return this.#h3handle.sendHeaders(
401+
stream[kStreamHandle], headerString, flags);
319402
}
320403

321404
/**
@@ -326,11 +409,12 @@ class Http3Stream {
326409
sendInformationalHeaders(headers) {
327410
const stream = this.#stream;
328411
if (stream.destroyed) return false;
412+
if (this.#h3handle === undefined) return false;
329413
validateObject(headers, 'headers');
330414
const headerString = buildNgHeaderString(
331415
headers, assertValidPseudoHeader, true);
332-
return stream[kStreamHandle].sendHeaders(
333-
kHeadersKindHints, headerString, kHeadersFlagsNone);
416+
return this.#h3handle.sendInformationalHeaders(
417+
stream[kStreamHandle], headerString);
334418
}
335419

336420
/**
@@ -342,11 +426,12 @@ class Http3Stream {
342426
sendTrailers(headers) {
343427
const stream = this.#stream;
344428
if (stream.destroyed) return false;
429+
if (this.#h3handle === undefined) return false;
345430
validateObject(headers, 'headers');
346431
const headerString =
347432
buildNgHeaderString(headers, assertValidPseudoHeaderTrailer);
348-
return stream[kStreamHandle].sendHeaders(
349-
kHeadersKindTrailing, headerString, kHeadersFlagsNone);
433+
return this.#h3handle.sendTrailers(
434+
stream[kStreamHandle], headerString);
350435
}
351436

352437
// Outbound body writer (stream/iter push writer).
@@ -371,6 +456,7 @@ class Http3Stream {
371456

372457
class Http3Session {
373458
#session;
459+
#h3handle;
374460
#onstream = undefined;
375461
#ongoaway = undefined;
376462
#onorigin = undefined;
@@ -395,6 +481,11 @@ class Http3Session {
395481
// routed back here.
396482
const handle = session[kSessionHandle];
397483
if (handle !== undefined) {
484+
if (handle[kApplicationOwner] !== undefined) {
485+
throw new ERR_INVALID_STATE(
486+
'The QUIC session already has an application attached');
487+
}
488+
this.#h3handle = createHttp3Handle(handle);
398489
handle[kApplicationOwner] = this;
399490
}
400491
// Claim the session's incoming streams. This takes the single
@@ -417,6 +508,9 @@ class Http3Session {
417508
if (onsettings !== undefined) this.onsettings = onsettings;
418509
}
419510

511+
// Internal: hands the per-session HTTP/3 handle to this session's streams.
512+
[kGetHttp3Handle]() { return this.#h3handle; }
513+
420514
/**
421515
* The peer initiated a graceful shutdown of the session (HTTP/3
422516
* GOAWAY). The session stops allowing new streams.
@@ -584,14 +678,22 @@ class Http3Session {
584678
onwanttrailers,
585679
onreset,
586680
onerror,
681+
priority,
682+
incremental,
587683
...quicOptions
588684
} = options;
589685

590686
let headerString;
591687
if (headers !== undefined) {
688+
let toSend = headers;
689+
const pri = priorityFieldValue(priority ?? 'default', incremental ?? false);
690+
if (pri !== undefined && !ObjectHasOwn(headers, 'priority')) {
691+
toSend = { __proto__: null, ...headers, priority: pri };
692+
}
592693
headerString = buildNgHeaderString(
593-
headers, assertValidPseudoHeader, true /* strictSingleValueFields */);
694+
toSend, assertValidPseudoHeader, true /* strictSingleValueFields */);
594695
}
696+
595697
const stream = await this.#session.createBidirectionalStream(quicOptions);
596698
const wrapped = new Http3Stream(stream, this, {
597699
__proto__: null,
@@ -602,6 +704,11 @@ class Http3Session {
602704
onreset,
603705
onerror,
604706
});
707+
708+
if (priority !== undefined || incremental !== undefined) {
709+
wrapped.setPriority({ __proto__: null, level: priority, incremental });
710+
}
711+
605712
// Submit the request headers only after the callbacks above are
606713
// attached. Nothing for this stream can hit the wire before the
607714
// headers are submitted: the HTTP/3 application only learns about the
@@ -611,8 +718,7 @@ class Http3Session {
611718
if (headerString !== undefined) {
612719
const flags = quicOptions.body === undefined ?
613720
kHeadersFlagsTerminal : kHeadersFlagsNone;
614-
if (!stream[kStreamHandle].sendHeaders(
615-
kHeadersKindInitial, headerString, flags)) {
721+
if (!wrapped[kSubmitInitialHeaders](headerString, flags)) {
616722
wrapped.destroy();
617723
throw new ERR_QUIC_OPEN_STREAM_FAILED();
618724
}

‎lib/internal/quic/quic.js‎

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,6 @@ const endpointRegistry = new SafeSet();
287287
* FileHandle|AsyncIterable|Iterable|Promise|null} [body] The outbound
288288
* body source. See the public docs for `stream.setBody()` for details
289289
* on supported types. When omitted, the stream is closed immediately.
290-
* @property {'high'|'default'|'low'} [priority] The priority level of the stream.
291-
* @property {boolean} [incremental] Whether to interleave data with same-priority streams.
292290
* @property {number} [budget] The byte budget for write backpressure.
293291
* **Default:** `65536`.
294292
*/
@@ -477,12 +475,6 @@ const endpointRegistry = new SafeSet();
477475
* frames do not themselves carry a reason field over the wire.
478476
*/
479477

480-
/**
481-
* @typedef {object} StreamPriority
482-
* @property {'default' | 'low' | 'high'} level The priority level of the stream.
483-
* @property {boolean} incremental Whether to interleave data with same-priority streams.
484-
*/
485-
486478
/**
487479
* @typedef {object} QuicSessionPath
488480
* @property {SocketAddress} local The local address for this path
@@ -2125,45 +2117,6 @@ class QuicStream {
21252117
this.#handle.resetStream(BigInt(code));
21262118
}
21272119

2128-
/**
2129-
* The priority of the stream. If the stream is destroyed or if
2130-
* the session does not support priority, `null` will be
2131-
* returned.
2132-
* @type {StreamPriority | null}
2133-
*/
2134-
get priority() {
2135-
assertIsQuicStream(this);
2136-
if (this.destroyed ||
2137-
!getQuicSessionState(this.#inner.session).isPrioritySupported) return null;
2138-
const packed = this.#handle.getPriority();
2139-
const urgency = packed >> 1;
2140-
const incremental = !!(packed & 1);
2141-
const level = urgency < 3 ? 'high' : urgency > 3 ? 'low' : 'default';
2142-
return { level, incremental };
2143-
}
2144-
2145-
/**
2146-
* Sets the priority of the stream.
2147-
* @param {StreamPriority} [options]
2148-
*/
2149-
setPriority(options = kEmptyObject) {
2150-
assertIsQuicStream(this);
2151-
if (this.destroyed) return;
2152-
if (!getQuicSessionState(this.#inner.session).isPrioritySupported) {
2153-
throw new ERR_INVALID_STATE(
2154-
'The session does not support stream priority');
2155-
}
2156-
validateObject(options, 'options');
2157-
const {
2158-
level = 'default',
2159-
incremental = false,
2160-
} = options;
2161-
validateOneOf(level, 'options.level', ['default', 'low', 'high']);
2162-
validateBoolean(incremental, 'options.incremental');
2163-
const urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3;
2164-
this.#handle.setPriority((urgency << 1) | (incremental ? 1 : 0));
2165-
}
2166-
21672120
[kFinishClose](error) {
21682121
const inner = this.#inner;
21692122
inner.pendingClose ??= PromiseWithResolvers();
@@ -2841,26 +2794,16 @@ class QuicSession {
28412794
validateObject(options, 'options');
28422795
const {
28432796
body,
2844-
priority = 'default',
2845-
incremental = false,
28462797
budget = kDefaultBudget,
28472798
} = options;
28482799

2849-
validateOneOf(priority, 'options.priority', ['default', 'low', 'high']);
2850-
validateBoolean(incremental, 'options.incremental');
2851-
28522800
const validatedBody = validateBody(body);
28532801

28542802
const handle = this.#handle.openStream(direction, validatedBody);
28552803
if (handle === undefined) {
28562804
throw new ERR_QUIC_OPEN_STREAM_FAILED();
28572805
}
28582806

2859-
if (inner.state.isPrioritySupported) {
2860-
const urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3;
2861-
handle.setPriority((urgency << 1) | (incremental ? 1 : 0));
2862-
}
2863-
28642807
const stream = new QuicStream(
28652808
kPrivateConstructor, handle, this, direction, true /* isLocal */);
28662809
inner.streams.add(stream);

0 commit comments

Comments
 (0)