Repository: electron/electron
Pull request: fix: `webRequest.onBeforeSendHeaders` not being able to modify reserved headers

Fix `webRequest.onBeforeSendHeaders` not being able to modify headers like `Proxy-Authorization` for requests made via the `net` module.

When using `webRequest.onBeforeSendHeaders` to inject headers like `Proxy-Authorization` into requests made via `net.request` or `net.fetch`, the request fails with `net::ERR_INVALID_ARGUMENT`. However, the same webRequest listener works correctly for renderer-initiated requests (e.g., `fetch()` from a BrowserWindow).  The network service validates headers and headers starting with `Proxy-` are rejected by `AreRequestHeadersSafe()` in `CorsURLLoaderFactory::IsValidRequest()`.

For renderer requests, this isn't a problem because:
1. The initial request passes validation (no `Proxy-Authorization` header yet)
2. Header modifications via webRequest happen through the `TrustedHeaderClient` callback, which occurs **after** validation
3. Modified headers are passed directly to the network layer, bypassing re-validation

For `net` module requests, the `TrustedHeaderClient` path was not being used because `network_service_request_id` was always `0`. This caused header modifications to go through the `FollowRedirect()` path instead, which re-validates headers with `AreRequestHeadersSafe()` and rejects `Proxy-*` headers.

The condition that gates `TrustedHeaderClient` usage (in `proxying_url_loader_factory.cc`):
```cpp
current_request_uses_header_client_ =
    factory_->url_loader_header_client_receiver_.is_bound() &&
    (for_cors_preflight_ || network_service_request_id_ != 0) &&
    has_any_extra_headers_listeners_;
```

## Solution

Set a non-zero request ID for `net` module requests using Chromium's `GlobalRequestID::MakeBrowserInitiated()`. This generates unique negative request IDs (-2, -3, -4, ...) that are distinct from renderer request IDs (positive numbers). This is the same mechanism Chromium uses internally for other browser-initiated requests.

Setting `Proxy-Authorization` directly in the `net.fetch` options will still fail, as the header would be present in the initial request before validation. The header must be injected via `webRequest.onBeforeSendHeaders` to use the `TrustedHeaderClient` path.

Notes: Requests sent via `net` are now capable of having their headers modified to use reserved headers via `webRequest`

Changed files:
filename: shell/browser/net/proxying_url_loader_factory.cc
patch: @@ -55,9 +55,9 @@ ProxyingURLLoaderFactory::InProgressRequest::InProgressRequest(
       proxied_loader_receiver_(this, std::move(loader_receiver)),
       target_client_(std::move(client)),
       current_response_(network::mojom::URLResponseHead::New()),
-      // Always use "extraHeaders" mode to be compatible with old APIs, except
-      // when the |request_id_| is zero, which is not supported in Chromium and
-      // only happens in Electron when the request is started from net module.
+      // Always use "extraHeaders" mode to be compatible with old APIs.
+      // A non-zero request ID is required to use the TrustedHeaderClient path
+      // which allows webRequest to modify headers like Proxy-Authorization.
       has_any_extra_headers_listeners_(network_service_request_id != 0) {
   // If there is a client error, clean up the request.
   target_client_.set_disconnect_handler(base::BindOnce(

filename: shell/common/api/electron_api_url_loader.cc
patch: @@ -17,6 +17,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/notreached.h"
 #include "base/sequence_checker.h"
+#include "content/public/browser/global_request_id.h"
 #include "gin/object_template_builder.h"
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/system/data_pipe_producer.h"
@@ -375,6 +376,13 @@ void SimpleURLLoaderWrapper::Start() {
 
   loader_->SetAllowHttpErrorResults(true);
   loader_->SetURLLoaderFactoryOptions(request_options_);
+  // Set a non-zero request ID so that the request can use the
+  // TrustedHeaderClient code path for webRequest header modifications.
+  // See proxying_url_loader_factory.cc for details.
+  if (electron::IsBrowserProcess()) {
+    loader_->SetRequestID(
+        content::GlobalRequestID::MakeBrowserInitiated().request_id);
+  }
   loader_->SetOnResponseStartedCallback(base::BindOnce(
       &SimpleURLLoaderWrapper::OnResponseStarted, weak_factory_.GetWeakPtr()));
   loader_->SetOnRedirectCallback(base::BindRepeating(

filename: spec/api-web-request-spec.ts
patch: @@ -1,4 +1,4 @@
-import { ipcMain, protocol, session, WebContents, webContents } from 'electron/main';
+import { ipcMain, net, protocol, session, WebContents, webContents } from 'electron/main';
 
 import { expect } from 'chai';
 import * as WebSocket from 'ws';
@@ -455,6 +455,35 @@ describe('webRequest module', () => {
       }));
       expect(onSendHeadersCalled).to.be.true();
     });
+
+    it('can inject Proxy-Authorization header for net module requests', async () => {
+      // Proxy-Authorization is normally rejected by Chromium's network service
+      // for security reasons. However, for Electron's trusted net module,
+      // webRequest.onBeforeSendHeaders should be able to inject it via the
+      // TrustedHeaderClient code path.
+      const proxyAuthValue = 'Basic test-credentials';
+      let receivedProxyAuth: string | undefined;
+
+      const server = http.createServer((req, res) => {
+        receivedProxyAuth = req.headers['proxy-authorization'];
+        res.end('ok');
+      });
+      const { url: serverUrl } = await listen(server);
+
+      try {
+        ses.webRequest.onBeforeSendHeaders((details, callback) => {
+          const requestHeaders = details.requestHeaders;
+          requestHeaders['Proxy-Authorization'] = proxyAuthValue;
+          callback({ requestHeaders });
+        });
+
+        const response = await net.fetch(serverUrl, { bypassCustomProtocolHandlers: true });
+        expect(response.ok).to.be.true();
+        expect(receivedProxyAuth).to.equal(proxyAuthValue);
+      } finally {
+        server.close();
+      }
+    });
   });
 
   describe('webRequest.onSendHeaders', () => {