Page Bridge & Injection
The page bridge is a fallback communication channel that allows Inspector Lab to function when the extension's background process cannot be reached. This is critical for environments like Orion on iOS and iPadOS where standard extension APIs may be unavailable or unreliable.
Overview
Inspector Lab operates in multiple isolated worlds within a web page:
- Isolated World: Where the extension's content script runs (limited capabilities)
- Main World: The page's own JavaScript realm (full page access, but no extension APIs)
- Inspector UI: The devtools panel itself (in a shadow DOM)
The main world is necessary for certain operations—particularly console evaluation and network/console hooks—because only code running there can access the page's global scope and intercept its functions. Normally, the background service worker uses chrome.scripting to inject code into the main world. When that's unavailable, the page bridge takes over.
The page bridge carries no extension privileges. Code executed through it has exactly the same authority as the page itself, and cannot bypass the page's Content Security Policy.
Architecture
Page Bridge Installation
The page bridge is loaded lazily when needed—typically when console evaluation or network capture is first requested and the background is unreachable. The installPageBridge() function in page-bridge-client.ts handles the installation:
Generate four unique channel names (random tokens) for bidirectional communication: connectEvent, evalRequestEvent, evalReplyEvent, and readyEvent.
Create an inline <script> element containing the page bridge source code. The configuration is passed via the script's data-inspector-lab-bridge attribute as JSON.
Listen for the readyEvent custom event. The page bridge fires this as soon as it runs synchronously on insertion—or a security policy violation event fires if CSP blocks the injection.
Remove the script element from the DOM (it's no longer needed after execution) and resolve with a client interface.
Timeout Behavior:
- Install timeout: 5 seconds (
INSTALL_TIMEOUT_MS) - Eval timeout: 10 seconds per evaluation (
EVAL_TIMEOUT_MS)
If the timeout expires or a securitypolicyviolation event fires for script-src, the install fails with a PageBridgeError describing the issue.
Configuration Protocol
The bridge receives its configuration as a JSON object on the script element's dataset:
type PageBridgeConfig = {
connectEvent: string;
evalRequestEvent: string;
evalReplyEvent: string;
readyEvent: string;
};This approach avoids any external dependencies or fetch operations—the bridge works even if the extension runtime has gone away entirely.
Communication Flow
Console Evaluation
When the inspector needs to evaluate an expression in the page's main world:
- Inspector UI → calls
evaluateExpression() - Tries background first → sends
EvaluateRequestto background service worker - On failure → sets
evaluateViaPage = trueand installs the page bridge - Page bridge client → dispatches a
CustomEventonevalRequestEvent - Page bridge (main world) → receives the event and calls
evaluateInPage() - Evaluation result → dispatched back as a custom event on
evalReplyEvent - Client resolves → with
EvaluateResponse(success or error message)
Each evaluation is assigned a unique ID to match requests with replies:
type PageBridgeEvalRequest = {
id: string;
expression: string;
limit: number; // Character limit for the preview
};
type PageBridgeEvalResponse = {
id: string;
response: EvaluateResponse;
};Three Core Services
The page bridge provides three services to replace what the background normally does in the main world:
-
Console Hooks — Installs and connects the console prehook (via
connect()) so thatconsole.log()and related calls are captured and sent to the inspector. -
Network Hooks — Installs and connects the network prehook so that
fetch()andXMLHttpRequestcalls are captured without requiring thewebRequestAPI. -
Expression Evaluation — Evaluates arbitrary JavaScript in the page's global scope, running the same
evaluateInPage()function that the background would inject.
All three services are automatically initialized when the prehooks are imported; both guard against duplicate installation, so this is a no-op when the document_start registration or an earlier launch already ran them.
Hook Connection
Network and console capture relies on hooks installed in the main world (via console-prehook and network-prehook modules). When the background cannot inject these, the page bridge connects them instead:
type PageBridgeConnectRequest = {
hook: "network" | "console";
eventName: string;
};The page bridge's connect() function:
- Locates the hook on the
windowobject (stored as__inspectorLabNetworkHookor__inspectorLabConsoleHook) - Sets its
eventNameproperty to the provided channel name - Replays any events buffered while the hook was unconnected
Limitations and Fallbacks
Content Security Policy
The most common failure point: if the page's CSP forbids inline scripts (script-src directive doesn't include 'unsafe-inline'), the bridge cannot inject itself. The client detects this via a securitypolicyviolation event and immediately reports:
This browser or this site's Content Security Policy blocks the in-page
fallback, and the extension background is unreachable. Browser DevTools
can bypass CSP; an in-page inspector cannot.Browser DevTools can bypass a page's CSP because they run in a privileged context. Inspector Lab, running as a content script, cannot. This is a fundamental browser security boundary.
Limited Cookie Access
When cookie operations fall back to document.cookie:
- Available: User-facing cookies (unencrypted, non-HttpOnly)
- Unavailable: HttpOnly cookies, Secure flag, SameSite settings
The inspector's Cookies panel labels fallback cookies as "document" source and explains the limitations.
Network Capture
Network capture via the page bridge relies on fetch and XMLHttpRequest hooks. The webRequest API (which sees all network traffic) is not available, so:
- Real-time capture during page load is limited to JavaScript-initiated requests
- Service Worker requests and image loads may not be visible
- Headers-only details from the background (via
webRequest) are not available as a fallback
Cookie Fallback Logic
Cookie operations have a three-state flow:
let cookieStoreUnreachable: { reason: string; runtimeGone: boolean } | null = null;- Initial state (
null): Try the background viachrome.cookies - First read fails: Note the reason, fall back to
document.cookiefor all subsequent reads and writes - Write operations: Only use the fallback if already marked unreachable; never trigger the fallback on their own
This prevents race conditions: a write timeout does not mean the message didn't arrive—it may have succeeded but lost its reply. Replaying the write against document.cookie could corrupt data.
Timeout Optimization: Once a read has confirmed the store is unreachable, subsequent reads use a shorter timeout (1.5 seconds) instead of the default, so the panel doesn't stall on every refresh.
XML and SVG Documents
The page bridge works in any document realm, including XML and SVG. The element-picking and inspection features work via DOM traversal (safe everywhere), but console evaluation and network hooks may behave differently in non-HTML contexts due to differences in the global object and available APIs.
See XML & SVG Document Support for more details.
Error Handling
PageBridgeError
Thrown by installPageBridge() when injection fails. Always carries a user-friendly message suitable for display in the UI, since no JavaScript console is available to the end user on devices like iPad.
export class PageBridgeError extends Error {}Silent Failures
Some failures are intentionally silent to avoid noise:
- Invalid event details (not JSON) are ignored
- A mismatched
idin an eval reply is ignored (may belong to a timed-out request) - If the bridge is called on a page that never installed the prehooks,
connect()is a no-op
Performance Notes
- Script injection overhead: ~0–5ms (synchronous execution)
- Per-evaluation overhead: 1–2ms for event dispatch and JSON serialization
- Memory: ~12 KB of source code, removed after successful installation
- No persistent state: Each page load gets a fresh bridge with new channel tokens
The bridge is a fallback, not a replacement: use the background path whenever possible. Chrome and Firefox with a healthy extension runtime should never hit this code path.