From 85f039d215223fad9c9a6e6139ee7fae47d43adc Mon Sep 17 00:00:00 2001 From: Amol Yadav Date: Sun, 15 Feb 2026 22:50:33 +0530 Subject: [PATCH] feat: add IP prioritization hints for HTTP/1.1 and HTTP/2 --- lib/core/request.js | 9 +++- lib/dispatcher/client-h1.js | 4 ++ lib/dispatcher/client.js | 2 +- test/ip-prioritization.js | 90 +++++++++++++++++++++++++++++++++++++ types/connector.d.ts | 1 + types/dispatcher.d.ts | 12 ++--- 6 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 test/ip-prioritization.js diff --git a/lib/core/request.js b/lib/core/request.js index 7dbf781b4c4..8591f90588c 100644 --- a/lib/core/request.js +++ b/lib/core/request.js @@ -44,7 +44,8 @@ class Request { expectContinue, servername, throwOnError, - maxRedirections + maxRedirections, + hints }, handler) { if (typeof path !== 'string') { throw new InvalidArgumentError('path must be a string') @@ -92,12 +93,18 @@ class Request { throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor') } + if (hints != null && (typeof hints !== 'number' || hints < 0)) { + throw new InvalidArgumentError('hints must be a positive number') + } + this.headersTimeout = headersTimeout this.bodyTimeout = bodyTimeout this.method = method + this.hints = hints ?? 0 + this.abort = null if (body == null) { diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index ce6b4eedbd3..d52712240b0 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -1114,6 +1114,10 @@ function writeH1 (client, request) { socket[kBlocking] = true } + if (socket.setPriority) { + socket.setPriority(request.hints) + } + let header = `${method} ${path} HTTP/1.1\r\n` if (typeof host === 'string') { diff --git a/lib/dispatcher/client.js b/lib/dispatcher/client.js index 101acb60123..6be866f973c 100644 --- a/lib/dispatcher/client.js +++ b/lib/dispatcher/client.js @@ -69,7 +69,7 @@ const getDefaultNodeMaxHeaderSize = http && ? () => http.maxHeaderSize : () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') } -const noop = () => {} +const noop = () => { } function getPipelining (client) { return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 diff --git a/test/ip-prioritization.js b/test/ip-prioritization.js new file mode 100644 index 00000000000..61261a6898e --- /dev/null +++ b/test/ip-prioritization.js @@ -0,0 +1,90 @@ +'use strict' + +const { test } = require('node:test') +const { Client } = require('..') +const { createServer } = require('node:http') +const { once } = require('node:events') + +test('HTTP/1.1 Request Prioritization', async (t) => { + let priority = null + + const server = createServer((req, res) => { + res.end('ok') + }) + + server.listen(0) + await once(server, 'listening') + + const client = new Client(`http://localhost:${server.address().port}`, { + connect: (opts, cb) => { + const socket = require('node:net').connect({ + ...opts, + host: opts.hostname, + port: opts.port + }, () => { + cb(null, socket) + }) + socket.setPriority = (p) => { + priority = p + } + return socket + } + }) + + try { + await client.request({ + path: '/', + method: 'GET', + hints: 42 + }) + + // Check if priority was set + if (priority !== 42) { + throw new Error(`Expected priority 42, got ${priority}`) + } + } finally { + await client.close() + server.close() + } +}) + +test('HTTP/2 Connection Prioritization', async (t) => { + const net = require('node:net') + const buildConnector = require('../lib/core/connect') + + let receivedHints = null + // Mock net.connect + t.mock.method(net, 'connect', (options) => { + receivedHints = options.hints + + const socket = new (require('node:events').EventEmitter)() + socket.cork = () => { } + socket.uncork = () => { } + socket.destroy = () => { } + socket.ref = () => { } + socket.unref = () => { } + socket.setKeepAlive = () => socket + socket.setNoDelay = () => socket + + // Simulate connection to allow callback to fire + process.nextTick(() => { + socket.emit('connect') + }) + + return socket + }) + + // Test buildConnector directly to ensure options passing + const connector = buildConnector({ hints: 123, allowH2: true }) + + await new Promise((resolve, reject) => { + connector({ hostname: 'localhost', host: 'localhost', protocol: 'http:', port: 3000 }, (err, socket) => { + if (err) reject(err) + else resolve(socket) + }) + }) + + if (receivedHints !== 123) { + throw new Error(`Expected hints 123, got ${receivedHints}`) + } +}) diff --git a/types/connector.d.ts b/types/connector.d.ts index 3376df7390f..01999a402b3 100644 --- a/types/connector.d.ts +++ b/types/connector.d.ts @@ -13,6 +13,7 @@ declare namespace buildConnector { port?: number; keepAlive?: boolean | null; keepAliveInitialDelay?: number | null; + hints?: number | null; } export interface Options { diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 684ca439c9a..4ceef8fee8f 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -96,7 +96,7 @@ declare class Dispatcher extends EventEmitter { } declare namespace Dispatcher { - export interface ComposedDispatcher extends Dispatcher {} + export interface ComposedDispatcher extends Dispatcher { } export type Dispatch = Dispatcher['dispatch'] export type DispatcherComposeInterceptor = (dispatch: Dispatch) => Dispatch export interface DispatchOptions { @@ -113,6 +113,8 @@ declare namespace Dispatcher { idempotent?: boolean; /** Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to `true` further pipelining will be avoided on the same connection until headers have been received. Defaults to `method !== 'HEAD'`. */ blocking?: boolean; + /** The objective priority of the resource. Default: `0` */ + hints?: number | null; /** Upgrade the request. Should be used to specify the kind of upgrade i.e. `'Websocket'`. Default: `method === 'CONNECT' || null`. */ upgrade?: boolean | string | null; /** The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers. Defaults to 300 seconds. */ @@ -213,10 +215,10 @@ declare namespace Dispatcher { export type StreamFactory = (data: StreamFactoryData) => Writable export interface DispatchController { - get aborted () : boolean - get paused () : boolean - get reason () : Error | null - abort (reason: Error): void + get aborted(): boolean + get paused(): boolean + get reason(): Error | null + abort(reason: Error): void pause(): void resume(): void }