From 2003bae67936073b15c16ecd090a5a431f1e3db3 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 | 95 ++++++++++++++++++++++--- 2 files changed, 126 insertions(+), 15 deletions(-) diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 96362355c1deba..872ffa4a3fd151 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -12,6 +12,8 @@ const { DataViewPrototypeGetByteLength, ErrorCaptureStackTrace, FunctionPrototypeBind, + MathMax, + MathMin, Number, ObjectDefineProperties, ObjectKeys, @@ -296,6 +298,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} [highWaterMark] The high water mark for write * backpressure, in bytes. **Default:** `65536`. @@ -513,6 +522,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. */ @@ -2443,7 +2455,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 }; } /** @@ -2461,10 +2475,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)); } @@ -3250,6 +3276,7 @@ class QuicSession { const { body, priority = 'default', + sendOrder, incremental = false, highWaterMark = kDefaultHighWaterMark, headers, @@ -3270,7 +3297,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 fc7ca231f0d63a..5616e3cfbd8fd9 100644 --- a/test/parallel/test-quic-h3-priority.mjs +++ b/test/parallel/test-quic-h3-priority.mjs @@ -14,7 +14,7 @@ import assert from 'node:assert'; import * as fixtures from '../common/fixtures.mjs'; const { readKey } = fixtures; -const { deepStrictEqual, strictEqual } = assert; +const { deepStrictEqual, strictEqual, rejects } = assert; if (!hasQuic) { skip('QUIC is not enabled'); @@ -39,6 +39,7 @@ const decoder = new TextDecoder(); const pri = stream.priority; strictEqual(typeof pri, 'object'); strictEqual(typeof pri.level, 'string'); + strictEqual(typeof pri.sendOrder, 'number'); strictEqual(typeof pri.incremental, 'boolean'); }, 4); }), { @@ -75,7 +76,7 @@ const decoder = new TextDecoder(); }); // Priority reflects what was set at creation. - deepStrictEqual(stream1.priority, { level: 'high', incremental: false }); + deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 0, incremental: false }); // Priority 'low' + incremental at creation. const stream2 = await clientSession.createBidirectionalStream({ @@ -91,7 +92,7 @@ const decoder = new TextDecoder(); strictEqual(headers[':status'], '200'); }), }); - deepStrictEqual(stream2.priority, { level: 'low', incremental: true }); + deepStrictEqual(stream2.priority, { level: 'low', sendOrder: 7, incremental: true }); // Default priority at creation. const stream3 = await clientSession.createBidirectionalStream({ @@ -105,7 +106,7 @@ const decoder = new TextDecoder(); strictEqual(headers[':status'], '200'); }), }); - deepStrictEqual(stream3.priority, { level: 'default', incremental: false }); + deepStrictEqual(stream3.priority, { level: 'default', sendOrder: 3, incremental: false }); // setPriority after creation. const stream4 = await clientSession.createBidirectionalStream({ @@ -120,19 +121,19 @@ const decoder = new TextDecoder(); }), }); // Default priority initially. - deepStrictEqual(stream4.priority, { level: 'default', incremental: false }); + deepStrictEqual(stream4.priority, { level: 'default', sendOrder: 3, incremental: false }); // Change to high. stream4.setPriority({ level: 'high' }); - deepStrictEqual(stream4.priority, { level: 'high', incremental: false }); + deepStrictEqual(stream4.priority, { level: 'high', sendOrder: 0, incremental: false }); // Change to incremental. stream4.setPriority({ level: 'low', incremental: true }); - deepStrictEqual(stream4.priority, { level: 'low', incremental: true }); + deepStrictEqual(stream4.priority, { level: 'low', sendOrder: 7, incremental: true }); // Back to default. stream4.setPriority({ level: 'default', incremental: false }); - deepStrictEqual(stream4.priority, { level: 'default', incremental: false }); + deepStrictEqual(stream4.priority, { level: 'default', sendOrder: 3, incremental: false }); // Read all bodies. const allBodies = await Promise.all([ @@ -177,7 +178,7 @@ const decoder = new TextDecoder(); // The server's priority getter should reflect the // client's PRIORITY_UPDATE (high, incremental). - deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + deepStrictEqual(stream.priority, { level: 'high', sendOrder: 0, incremental: true }); serverSawHighPriority.resolve(); await stream.closed; @@ -221,14 +222,14 @@ const decoder = new TextDecoder(); strictEqual(headers[':status'], '200'); }), }); - deepStrictEqual(stream.priority, { level: 'default', incremental: false }); + 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 }); - deepStrictEqual(stream.priority, { level: 'high', incremental: true }); + deepStrictEqual(stream.priority, { level: 'high', sendOrder: 0, incremental: true }); // Read the response. const body = await bytes(stream); @@ -241,3 +242,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) => { + 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, + }); + deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 1, incremental: false }); + + // Update sendOrder via setPriority. + stream1.setPriority({ sendOrder: 6, incremental: true }); + 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) + deepStrictEqual(streamNeg.priority, { level: 'high', sendOrder: 0, incremental: false }); + + const streamLarge = await clientSession.createBidirectionalStream({ sendOrder: 999 }); + // Clamped to 7 (lowest urgency) + deepStrictEqual(streamLarge.priority, { level: 'low', sendOrder: 7, incremental: false }); + + // Non-integer values are still rejected. + await rejects( + clientSession.createBidirectionalStream({ sendOrder: 1.5 }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + await rejects( + clientSession.createBidirectionalStream({ sendOrder: '3' }), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + + // setPriority also accepts and clamps out-of-range integers. + stream1.setPriority({ sendOrder: -5 }); + deepStrictEqual(stream1.priority, { level: 'high', sendOrder: 0, incremental: false }); + stream1.setPriority({ sendOrder: 100 }); + deepStrictEqual(stream1.priority, { level: 'low', sendOrder: 7, incremental: false }); + + const body = await bytes(stream1); + strictEqual(decoder.decode(body), 'ok'); + + await Promise.all([stream1.closed]); + await clientSession.close(); + await serverEndpoint.close(); +}