From dd90396f5646c6f51d67d0155d540b6c5b351270 Mon Sep 17 00:00:00 2001 From: SparshGarg999 Date: Sun, 12 Jul 2026 20:50:31 +0530 Subject: [PATCH] quic: align stream priority with W3C WebTransport sendOrder Adds the numeric sendOrder option to createBidirectionalStream, createUnidirectionalStream, and setPriority, aligning with the W3C WebTransport sendOrder concept where a lower value indicates a higher priority. This maps directly to the raw QUIC stream urgency value (an integer in the range [0, 7]). Additionally, the priority getter is updated to return sendOrder`nrepresenting the current raw QUIC stream urgency value. Fixes: https://github.com/nodejs/node/issues/63654 Signed-off-by: SparshGarg999 --- lib/internal/quic/quic.js | 46 ++++++++++-- test/parallel/test-quic-h3-priority.mjs | 93 ++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 14 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 8ecd5fc776a3..6d4b9f7367bf 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -13,6 +13,8 @@ const { ErrorCaptureStackTrace, FunctionPrototypeBind, FunctionPrototypeCall, + MathMax, + MathMin, Number, ObjectDefineProperties, ObjectKeys, @@ -298,6 +300,13 @@ const endpointRegistry = new SafeSet(); * send. Only used when the negotiated application supports headers * (e.g. HTTP/3). * @property {'high'|'default'|'low'} [priority] The priority level of the stream. + * @property {number} [sendOrder] A signed integer representing the send order, + * aligned with the W3C WebTransport `sendOrder` concept. Any integer is + * accepted; values are clamped to the QUIC urgency range [0, 7]. Note that + * QUIC urgency semantics are inverted relative to W3C: a lower urgency value + * means *higher* priority. When specified, overrides `priority`. + * Full W3C `sendOrder` semantics (including `sendGroups`) require upstream + * ngtcp2 support and are not yet available. * @property {boolean} [incremental] Whether to interleave data with same-priority streams. * @property {number} [budget] The byte budget for write backpressure. * **Default:** `65536`. @@ -518,6 +527,9 @@ const endpointRegistry = new SafeSet(); /** * @typedef {object} StreamPriority * @property {'default' | 'low' | 'high'} level The priority level of the stream. + * @property {number} sendOrder The QUIC urgency value in [0, 7] used by + * this stream. Lower values mean higher priority. This corresponds to the + * clamped input from W3C WebTransport `sendOrder`. * @property {boolean} incremental Whether to interleave data with same-priority streams. */ @@ -2508,7 +2520,9 @@ class QuicStream { const urgency = packed >> 1; const incremental = !!(packed & 1); const level = urgency < 3 ? 'high' : urgency > 3 ? 'low' : 'default'; - return { level, incremental }; + // `sendOrder` exposes the raw QUIC urgency value (0-7), aligned with + // the W3C WebTransport sendOrder concept (lower value = higher priority). + return { level, sendOrder: urgency, incremental }; } /** @@ -2526,10 +2540,22 @@ class QuicStream { const { level = 'default', incremental = false, + sendOrder, } = options; - validateOneOf(level, 'options.level', ['default', 'low', 'high']); validateBoolean(incremental, 'options.incremental'); - const urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3; + let urgency; + if (sendOrder !== undefined) { + // sendOrder is aligned with the W3C WebTransport sendOrder concept. + // W3C accepts any long long; we validate it is a finite integer and + // clamp to the QUIC urgency range [0, 7]. Note: QUIC urgency has + // inverted priority direction vs W3C sendOrder. Full W3C semantics + // (sendGroups, full range) require upstream ngtcp2 support. + validateInteger(sendOrder, 'options.sendOrder'); + urgency = MathMax(0, MathMin(7, sendOrder)); + } else { + validateOneOf(level, 'options.level', ['default', 'low', 'high']); + urgency = level === 'high' ? 0 : level === 'low' ? 7 : 3; + } this.#handle.setPriority((urgency << 1) | (incremental ? 1 : 0)); } @@ -3356,6 +3382,7 @@ class QuicSession { const { body, priority = 'default', + sendOrder, incremental = false, budget = kDefaultBudget, headers, @@ -3376,7 +3403,18 @@ class QuicSession { } if (inner.state.isPrioritySupported) { - const urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3; + let urgency; + if (sendOrder !== undefined) { + // sendOrder is aligned with the W3C WebTransport sendOrder concept. + // W3C accepts any long long; we validate it is a finite integer and + // clamp to the QUIC urgency range [0, 7]. Note: QUIC urgency has + // inverted priority direction vs W3C sendOrder. Full W3C semantics + // (sendGroups, full range) require upstream ngtcp2 support. + validateInteger(sendOrder, 'options.sendOrder'); + urgency = MathMax(0, MathMin(7, sendOrder)); + } else { + urgency = priority === 'high' ? 0 : priority === 'low' ? 7 : 3; + } handle.setPriority((urgency << 1) | (incremental ? 1 : 0)); } diff --git a/test/parallel/test-quic-h3-priority.mjs b/test/parallel/test-quic-h3-priority.mjs index 10be3d6f216e..25b7fe6b7831 100644 --- a/test/parallel/test-quic-h3-priority.mjs +++ b/test/parallel/test-quic-h3-priority.mjs @@ -36,6 +36,7 @@ const decoder = new TextDecoder(); const pri = stream.priority; assert.strictEqual(typeof pri, 'object'); assert.strictEqual(typeof pri.level, 'string'); + assert.strictEqual(typeof pri.sendOrder, 'number'); assert.strictEqual(typeof pri.incremental, 'boolean'); }, 4); }), { @@ -72,7 +73,7 @@ const decoder = new TextDecoder(); }); // Priority reflects what was set at creation. - assert.deepStrictEqual(stream1.priority, { level: 'high', incremental: false }); + assert.deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 0, incremental: false }); // Priority 'low' + incremental at creation. const stream2 = await clientSession.createBidirectionalStream({ @@ -88,7 +89,7 @@ const decoder = new TextDecoder(); assert.strictEqual(headers[':status'], 200); }), }); - assert.deepStrictEqual(stream2.priority, { level: 'low', incremental: true }); + assert.deepStrictEqual(stream2.priority, { level: 'low', sendOrder: 7, incremental: true }); // Default priority at creation. const stream3 = await clientSession.createBidirectionalStream({ @@ -102,7 +103,7 @@ const decoder = new TextDecoder(); assert.strictEqual(headers[':status'], 200); }), }); - assert.deepStrictEqual(stream3.priority, { level: 'default', incremental: false }); + assert.deepStrictEqual(stream3.priority, { level: 'default', sendOrder: 3, incremental: false }); // setPriority after creation. const stream4 = await clientSession.createBidirectionalStream({ @@ -117,19 +118,19 @@ const decoder = new TextDecoder(); }), }); // Default priority initially. - assert.deepStrictEqual(stream4.priority, { level: 'default', incremental: false }); + assert.deepStrictEqual(stream4.priority, { level: 'default', sendOrder: 3, incremental: false }); // Change to high. stream4.setPriority({ level: 'high' }); - assert.deepStrictEqual(stream4.priority, { level: 'high', incremental: false }); + assert.deepStrictEqual(stream4.priority, { level: 'high', sendOrder: 0, incremental: false }); // Change to incremental. stream4.setPriority({ level: 'low', incremental: true }); - assert.deepStrictEqual(stream4.priority, { level: 'low', incremental: true }); + assert.deepStrictEqual(stream4.priority, { level: 'low', sendOrder: 7, incremental: true }); // Back to default. stream4.setPriority({ level: 'default', incremental: false }); - assert.deepStrictEqual(stream4.priority, { level: 'default', incremental: false }); + assert.deepStrictEqual(stream4.priority, { level: 'default', sendOrder: 3, incremental: false }); // Read all bodies. const allBodies = await Promise.all([ @@ -174,7 +175,7 @@ const decoder = new TextDecoder(); // The server's priority getter should reflect the // client's PRIORITY_UPDATE (high, incremental). - assert.deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + assert.deepStrictEqual(stream.priority, { level: 'high', sendOrder: 0, incremental: true }); serverSawHighPriority.resolve(); await stream.closed; @@ -218,14 +219,14 @@ const decoder = new TextDecoder(); assert.strictEqual(headers[':status'], 200); }), }); - assert.deepStrictEqual(stream.priority, { level: 'default', incremental: false }); + assert.deepStrictEqual(stream.priority, { level: 'default', sendOrder: 3, incremental: false }); // Change priority — this sends a PRIORITY_UPDATE frame on the // control stream. The body data was already provided at creation // but the PRIORITY_UPDATE travels on the control stream which // nghttp3 prioritizes over bidi streams. stream.setPriority({ level: 'high', incremental: true }); - assert.deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + assert.deepStrictEqual(stream.priority, { level: 'high', sendOrder: 0, incremental: true }); // Read the response. const body = await bytes(stream); @@ -238,3 +239,75 @@ const decoder = new TextDecoder(); await clientSession.close(); await serverEndpoint.close(); } + +// Test: sendOrder option and validation. +{ + const serverEndpoint = await listen(mustCall(async (ss) => { + ss.onstream = mustCall((stream) => { + assert.deepStrictEqual(stream.priority, { level: 'high', sendOrder: 1, incremental: false }); + }); + }), { + sni: { '*': { keys: [key], certs: [cert] } }, + onheaders: mustCall(function(headers) { + this.sendHeaders({ ':status': '200' }); + this.writer.writeSync(encoder.encode('ok')); + this.writer.endSync(); + }), + }); + + const clientSession = await connect(serverEndpoint.address, { + servername: 'localhost', + verifyPeer: 'manual', + }); + await clientSession.opened; + + // Set sendOrder at creation time. + const stream1 = await clientSession.createBidirectionalStream({ + headers: { + ':method': 'GET', + ':path': '/', + ':scheme': 'https', + ':authority': 'localhost', + }, + sendOrder: 1, + incremental: false, + }); + assert.deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 1, incremental: false }); + + // Update sendOrder via setPriority. + stream1.setPriority({ sendOrder: 6, incremental: true }); + assert.deepStrictEqual(stream1.priority, { level: 'low', sendOrder: 6, incremental: true }); + + // sendOrder accepts any integer per W3C long long semantics; + // values outside [0, 7] are clamped to the QUIC urgency range. + const streamNeg = await clientSession.createBidirectionalStream({ sendOrder: -100 }); + // Clamped to 0 (highest urgency) + assert.deepStrictEqual(streamNeg.priority, { level: 'high', sendOrder: 0, incremental: false }); + + const streamLarge = await clientSession.createBidirectionalStream({ sendOrder: 999 }); + // Clamped to 7 (lowest urgency) + assert.deepStrictEqual(streamLarge.priority, { level: 'low', sendOrder: 7, incremental: false }); + + // Non-integer values are still rejected. + await assert.rejects( + clientSession.createBidirectionalStream({ sendOrder: 1.5 }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + await assert.rejects( + clientSession.createBidirectionalStream({ sendOrder: '3' }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + + // setPriority also accepts and clamps out-of-range integers. + stream1.setPriority({ sendOrder: -5 }); + assert.deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 0, incremental: false }); + stream1.setPriority({ sendOrder: 100 }); + assert.deepStrictEqual(stream1.priority, { level: 'low', sendOrder: 7, incremental: false }); + + const body = await bytes(stream1); + assert.strictEqual(decoder.decode(body), 'ok'); + + await Promise.all([stream1.closed]); + await clientSession.close(); + await serverEndpoint.close(); +}