Repository: electron/electron
Pull request: feat: add bypassCustomProtocolHandlers option to net.request

#### Description of Change
This PR adds the `bypassCustomProtocolHandlers` option to `net.request()`, making it consistent with `net.fetch()` which already has this parameter. 
Currently, when using custom protocol handlers registered via `protocol.handle()`, there's a potential issue with circular interception when a handler needs to make network requests. While `net.fetch()` provides the `bypassCustomProtocolHandlers` parameter to prevent this, `net.request()` (which is used internally by `net.fetch()`) doesn't expose this option directly.

Close #47098 

#### Checklist

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Add bypassCustomProtocolHandlers option to net.request


Changed files:
filename: docs/api/client-request.md
patch: @@ -25,6 +25,11 @@ following properties:
     with which the request is associated. Defaults to the empty string. The
     `session` option supersedes `partition`. Thus if a `session` is explicitly
     specified, `partition` is ignored.
+  * `bypassCustomProtocolHandlers` boolean (optional) - When set to `true`,
+    custom protocol handlers registered for the request's URL scheme will not be
+    called. This allows forwarding an intercepted request to the built-in
+    handler. [webRequest](web-request.md) handlers will still be triggered
+    when bypassing custom protocols. Defaults to `false`.
   * `credentials` string (optional) - Can be `include`, `omit` or
     `same-origin`. Whether to send
     [credentials](https://fetch.spec.whatwg.org/#credentials) with this

filename: lib/common/api/net-client-request.ts
patch: @@ -289,7 +289,8 @@ function parseOptions (optionsIn: ClientRequestConstructorOptions | string): Nod
     referrerPolicy: options.referrerPolicy,
     cache: options.cache,
     allowNonHttpProtocols: Object.hasOwn(options, kAllowNonHttpProtocols),
-    priority: options.priority
+    priority: options.priority,
+    bypassCustomProtocolHandlers: options.bypassCustomProtocolHandlers
   };
   if ('priorityIncremental' in options) {
     urlLoaderOptions.priorityIncremental = options.priorityIncremental;

filename: spec/api-protocol-spec.ts
patch: @@ -15,6 +15,7 @@ import * as webStream from 'node:stream/web';
 import { setTimeout } from 'node:timers/promises';
 import * as url from 'node:url';
 
+import { collectStreamBody, getResponse } from './lib/net-helpers';
 import { listen, defer, ifit } from './lib/spec-helpers';
 import { WebmGenerator } from './lib/video-helpers';
 import { closeAllWindows, closeWindow } from './lib/window-helpers';
@@ -1578,6 +1579,22 @@ describe('protocol module', () => {
       expect(await net.fetch(url, { bypassCustomProtocolHandlers: true }).then(r => r.text())).to.equal('default');
     });
 
+    it('can bypass intercepted protocol handlers with net.request', async () => {
+      protocol.handle('http', () => new Response('custom'));
+      defer(() => { protocol.unhandle('http'); });
+      const server = http.createServer((req, res) => {
+        res.end('default');
+      });
+      defer(() => server.close());
+      const { url } = await listen(server);
+      // Make a request using net.request with bypassCustomProtocolHandlers: true
+      const request = net.request({ method: 'GET', url, bypassCustomProtocolHandlers: true });
+      const response = await getResponse(request);
+      const body = await collectStreamBody(response);
+      expect(response.statusCode).to.equal(200);
+      expect(body).to.equal('default');
+    });
+
     it('bypassing custom protocol handlers also bypasses new protocols', async () => {
       protocol.handle('app', () => new Response('custom'));
       defer(() => { protocol.unhandle('app'); });