From b328de39e560739092d06991466730e733f2bddc Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Sun, 8 Jun 2025 13:01:32 +0200 Subject: [PATCH] feat: [net] add "priority" option to net.request (#47320) document the default value of priority option Update the priority test to not use the httpbin.org as server Fixed the lint errors Fixed the build error Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Zeeker <13848632+zeeker999@users.noreply.github.com> --- docs/api/client-request.md | 4 ++ lib/common/api/net-client-request.ts | 6 +- shell/common/api/electron_api_url_loader.cc | 18 +++++ spec/api-net-spec.ts | 75 ++++++++++++++++++++- typings/internal-ambient.d.ts | 2 + 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/docs/api/client-request.md b/docs/api/client-request.md index 21490bf1f101..d6e27d6deb95 100644 --- a/docs/api/client-request.md +++ b/docs/api/client-request.md @@ -60,6 +60,10 @@ following properties: `strict-origin-when-cross-origin`. * `cache` string (optional) - can be `default`, `no-store`, `reload`, `no-cache`, `force-cache` or `only-if-cached`. + * `priority` string (optional) - can be `throttled`, `idle`, `lowest`, + `low`, `medium`, or `highest`. Defaults to `idle`. + * `priorityIncremental` boolean (optional) - the incremental loading flag as part + of HTTP extensible priorities (RFC 9218). Default is `true`. `options` properties such as `protocol`, `host`, `hostname`, `port` and `path` strictly follow the Node.js model as described in the diff --git a/lib/common/api/net-client-request.ts b/lib/common/api/net-client-request.ts index 1a087ae47647..a2682da670d2 100644 --- a/lib/common/api/net-client-request.ts +++ b/lib/common/api/net-client-request.ts @@ -288,8 +288,12 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod origin: options.origin, referrerPolicy: options.referrerPolicy, cache: options.cache, - allowNonHttpProtocols: Object.hasOwn(options, kAllowNonHttpProtocols) + allowNonHttpProtocols: Object.hasOwn(options, kAllowNonHttpProtocols), + priority: options.priority }; + if ('priorityIncremental' in options) { + urlLoaderOptions.priorityIncremental = options.priorityIncremental; + } const headers: Record = options.headers || {}; for (const [name, value] of Object.entries(headers)) { validateHeader(name, value); diff --git a/shell/common/api/electron_api_url_loader.cc b/shell/common/api/electron_api_url_loader.cc index be953ab7fea1..099fcd8d5963 100644 --- a/shell/common/api/electron_api_url_loader.cc +++ b/shell/common/api/electron_api_url_loader.cc @@ -644,6 +644,24 @@ gin::Handle SimpleURLLoaderWrapper::Create( break; } + if (std::string priority; opts.Get("priority", &priority)) { + static constexpr auto Lookup = + base::MakeFixedFlatMap({ + {"throttled", net::THROTTLED}, + {"idle", net::IDLE}, + {"lowest", net::LOWEST}, + {"low", net::LOW}, + {"medium", net::MEDIUM}, + {"highest", net::HIGHEST}, + }); + if (auto iter = Lookup.find(priority); iter != Lookup.end()) + request->priority = iter->second; + } + if (bool priorityIncremental = request->priority_incremental; + opts.Get("priorityIncremental", &priorityIncremental)) { + request->priority_incremental = priorityIncremental; + } + const bool use_session_cookies = opts.ValueOrDefault("useSessionCookies", false); int options = network::mojom::kURLLoadOptionSniffMimeType; diff --git a/spec/api-net-spec.ts b/spec/api-net-spec.ts index 5d98385d60f4..dbbab83c8947 100644 --- a/spec/api-net-spec.ts +++ b/spec/api-net-spec.ts @@ -1,15 +1,19 @@ -import { net, ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main'; +import { net, session, ClientRequest, ClientRequestConstructorOptions, utilityProcess } from 'electron/main'; import { expect } from 'chai'; import { once } from 'node:events'; +import * as fs from 'node:fs'; import * as http from 'node:http'; +import * as http2 from 'node:http2'; import * as path from 'node:path'; import { setTimeout } from 'node:timers/promises'; import { collectStreamBody, collectStreamBodyBuffer, getResponse, kOneKiloByte, kOneMegaByte, randomBuffer, randomString, respondNTimes, respondOnce } from './lib/net-helpers'; +import { listen, defer } from './lib/spec-helpers'; const utilityFixturePath = path.resolve(__dirname, 'fixtures', 'api', 'utility-process', 'api-net-spec.js'); +const fixturesPath = path.resolve(__dirname, 'fixtures'); async function itUtility (name: string, fn?: Function, args?: {[key:string]: any}) { it(`${name} in utility process`, async () => { @@ -46,6 +50,34 @@ describe('net module', () => { } }); + let http2URL: string; + + const certPath = path.join(fixturesPath, 'certificates'); + const h2server = http2.createSecureServer({ + key: fs.readFileSync(path.join(certPath, 'server.key')), + cert: fs.readFileSync(path.join(certPath, 'server.pem')) + }, async (req, res) => { + if (req.method === 'POST') { + const chunks = []; + for await (const chunk of req) chunks.push(chunk); + res.end(Buffer.concat(chunks).toString('utf8')); + } else if (req.method === 'GET' && req.headers[':path'] === '/get') { + res.end(JSON.stringify({ + headers: req.headers + })); + } else { + res.end(''); + } + }); + + before(async () => { + http2URL = (await listen(h2server)).url + '/'; + }); + + after(() => { + h2server.close(); + }); + for (const test of [itIgnoringArgs, itUtility]) { describe('HTTP basics', () => { test('should be able to issue a basic GET request', async () => { @@ -1615,4 +1647,45 @@ describe('net module', () => { }); }); } + + for (const test of [itIgnoringArgs]) { + describe('ClientRequest API', () => { + for (const [priorityName, urgency] of Object.entries({ + throttled: 'u=5', + idle: 'u=4', + lowest: '', + low: 'u=2', + medium: 'u=1', + highest: 'u=0' + })) { + for (const priorityIncremental of [true, false]) { + test(`should set priority to ${priorityName}/${priorityIncremental} if requested`, async () => { + // Priority header is available on HTTP/2, which is only + // supported over TLS, so... + session.defaultSession.setCertificateVerifyProc((req, cb) => cb(0)); + defer(() => { + session.defaultSession.setCertificateVerifyProc(null); + }); + + const urlRequest = net.request({ + url: `${http2URL}get`, + priority: priorityName as any, + priorityIncremental + }); + const response = await getResponse(urlRequest); + const data = JSON.parse(await collectStreamBody(response)); + let expectedPriority = urgency; + if (priorityIncremental) { + expectedPriority = expectedPriority ? expectedPriority + ', i' : 'i'; + } + if (expectedPriority === '') { + expect(data.headers.priority).to.be.undefined(); + } else { + expect(data.headers.priority).to.be.a('string').and.equal(expectedPriority); + } + }, { priorityName, urgency, priorityIncremental }); + } + } + }); + } }); diff --git a/typings/internal-ambient.d.ts b/typings/internal-ambient.d.ts index 21e652df5e45..2f0e6b87f0e4 100644 --- a/typings/internal-ambient.d.ts +++ b/typings/internal-ambient.d.ts @@ -177,6 +177,8 @@ declare namespace NodeJS { mode?: string; destination?: string; bypassCustomProtocolHandlers?: boolean; + priority?: 'throttled' | 'idle' | 'lowest' | 'low' | 'medium' | 'highest'; + priorityIncremental?: boolean; }; type ResponseHead = { statusCode: number;