Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const {
DataViewPrototypeGetByteLength,
ErrorCaptureStackTrace,
FunctionPrototypeBind,
MathMax,
MathMin,
Number,
ObjectDefineProperties,
ObjectKeys,
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
*/

Expand Down Expand Up @@ -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 };
}

/**
Expand All @@ -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));
}

Expand Down Expand Up @@ -3250,6 +3276,7 @@ class QuicSession {
const {
body,
priority = 'default',
sendOrder,
incremental = false,
highWaterMark = kDefaultHighWaterMark,
headers,
Expand All @@ -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));
}

Expand Down
95 changes: 84 additions & 11 deletions test/parallel/test-quic-h3-priority.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
}), {
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand All @@ -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({
Expand All @@ -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([
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}