feat: observational chrome.webRequest support#183
Conversation
Adds a small EventEmitter to ExtensionRouter which emits 'listener-added' and 'listener-removed' with (eventName, extensionId). This allows API implementations to lazily create backing resources only while extension listeners exist. Removals via extension unload and host destruction are also emitted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements chrome.webRequest events backed by Electron's
session.webRequest module.
Main process (WebRequestAPI):
- Observational only: Electron's blocking-capable callbacks are
resolved immediately with {} before fanning out to extensions.
- Lazily attaches a session.webRequest listener per event only while at
least one extension listens for it, and detaches when the last
extension listener is removed. This matters because registering any
session.webRequest listener disables Chromium's built-in extension
webRequest and declarativeNetRequest handling for new
URLLoaderFactories in the session.
- Maps Electron listener details to Chrome webRequest details,
including resource types, headers, request bodies, and tab IDs.
- Events are sent per-extension and gated on the webRequest permission
and manifest host permissions to avoid leaking request URLs.
Renderer preload:
- Full chrome.webRequest surface with all nine events,
handlerBehaviorChanged, and
MAX_HANDLER_BEHAVIOR_CHANGED_CALLS_PER_10_MINUTES.
- Listeners are filtered by RequestFilter (urls/types/tabId) and
headers/request bodies are removed unless requested via extraInfoSpec.
- onAuthRequired can be registered but never emits since Electron
provides no session.webRequest equivalent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a chrome-webRequest fixture extension which records received events and a spec covering: - onBeforeRequest/onCompleted details for page loads - subresource (fetch) events - RequestFilter URL filtering - header visibility gated on extraInfoSpec - lazy attach/detach of session.webRequest listeners Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chromium now exposes a native `browser` global in extension contexts as a distinct object from `chrome`. Cross-browser extensions commonly resolve `globalThis.browser || globalThis.chrome`, so they previously saw only the unpatched native APIs. Inject the same API objects into both namespaces. `browser` is deliberately not frozen: Chromium leaves it configurable, and extensions that wrap it in a Proxy returning substitute objects from get() would violate the proxy invariant for frozen data properties. Also make ChromeSetting stubs return Chrome-shaped results (value + levelOfControl: 'not_controllable') as promises or via callback — the previous bare stubs returned undefined, crashing extensions that chain .then() on privacy settings (observed with NordPass, which aborts its initialization before registering its action.onClicked listener).
|
Pushed a follow-up commit after validating against a real-world extension (NordPass, MV3, worker-driven action button):
With these plus the webRequest backend, NordPass's MV3 service worker goes from crashing at startup to fully initializing — registering |
|
This breaks adblockers somewhat unfortunately, tested on Electron 41 and 43 against ads like Adchoice (with Adguard/uBlock/uBlock Lite). The website I'm testing with for issues is spriters-resource.com, Adchoice is shoved in everywhere there. |
|
I should clarify that I did try with uBlock Lite in Electron 43 with and without the PR (and it successfully blocking the above mentioned site's ads only without the PR), since you mentioned that adblocker in your first post. |
What
Implements observational
chrome.webRequestsupport: a main-processWebRequestAPIbridging Electron'ssession.webRequestto extension contexts, and a full preload surface replacing the previousonHeadersReceived-only stub (which registered listeners that never fired).Why
MV3 extensions that observe network activity (password managers, privacy tools) currently crash at startup in Electron-based browsers: Electron registers the
webRequestschema but the API is non-functional in extension contexts, sochrome.webRequest.onBeforeRequestisundefined, the service worker throws, and MV3 worker registration fails outright. Context and root-cause analysis in electron/electron#52265 — notably, even on Electronmain(where the Electron 43 bindings-pak regression is fixed), webRequest events are never dispatched to MV3 service workers; MV2 background pages work. This PR makes the polyfill cover the gap for both.Verified end-to-end on Electron 43: an MV3 extension whose worker calls
chrome.webRequest.onBeforeRequest.addListener(...)at top level — previously an instant crash ("Service worker registration failed. Status code: 15") — now registers and receives events for page loads.Design notes
{}immediately, before fan-out.'blocking'/'extraHeaders'inextraInfoSpecare silently ignored, and listener return values have no effect. (MV3 Chrome itself restricts blocking webRequest to policy installs, so observational is the meaningful MV3 surface.)session.webRequestlistener disables Chromium's native extension webRequest anddeclarativeNetRequestenforcement for new URLLoaderFactories (Electron's exclusivity rule),WebRequestAPIattaches a session listener per event only on the 0→1 extension-listener transition and detaches on the last removal. Sessions/events with no webRequest listeners are untouched, so dNR-only extensions (e.g. uBO Lite dynamic rules) keep native enforcement. Implemented via newlistener-added/listener-removedevents onExtensionRouter.router.sendEvent(never broadcast), gated on thewebRequestmanifest permission plus host-pattern matching (host_permissionsMV3 /permissionsMV2,<all_urls>), so URLs don't leak to extensions without access. Service-worker delivery reuses the router's existingstartWorkerForScopewake-up.requestId, Chrome resource-type names (xhr→xmlhttprequest, etc.), headers as[{name, value}],requestBody.rawfromuploadData,statusCode/statusLine/fromCache/ip/redirectUrl/errorwhere Electron provides them;tabIdis-1for untracked webContents;frameId/parentFrameIdapproximated fromframeTreeNodeId(consistent withweb-navigation.ts).RequestFilter(urlsvia Chrome match-pattern semantics,types,tabId) and stripsrequestHeaders/responseHeaders/requestBodyunless requested viaextraInfoSpec.handlerBehaviorChangedis an async no-op.onAuthRequiredis registrable but never emits (nosession.webRequestequivalent).Known limitations vs Chrome
onAuthRequiredevents.initiator,documentId,documentLifecycle,frameType; noformDataparsing;filter.windowIdunsupported; runtime-granted optional host permissions not consulted.Tests
New
spec/chrome-webRequest-spec.ts+spec/fixtures/chrome-webRequest/: 6 specs covering event details (url/method/type/tabId/frameId/requestId), subresource typing, URL filtering, extraInfoSpec header gating, detach-on-removal, and lazy attach (no session listeners without extension listeners). Full package suite: 70 passing, 0 failing (2 pre-existing pending).build(tsc + esbuild) clean.🤖 Generated with Claude Code