Message Protocol
Inspector Lab communicates between multiple isolated realms—the extension background, the content script, and the page's main world—using a message-passing protocol built on Chrome's runtime messaging and DOM events.
Overview
The message protocol serves three core purposes:
- Background Communication — DevTools panels communicate with the extension's background service worker via
chrome.runtime.sendMessage, which handles cross-origin requests, cookie access, and network inspection. - Page Bridge — The content script injects a bridge into the page's main JavaScript realm, enabling console capture and code evaluation that the content script cannot perform directly.
- Event Channels — Unguessable event names act as secure channels, preventing page scripts from eavesdropping or spoofing inspector data.
Runtime Messages
All messages sent to the background follow a { type, ...payload } shape and return typed responses with an ok boolean flag.
Ping (Health Check)
Reachability probe answered synchronously by the background. It is the only handler that does not hold the reply channel open across an await, making it a discriminator: a runtime where PING answers but every other message comes back empty indicates a problem dropping the asynchronous reply channel, which is distinct from a background that never ran.
export type PingRequest = {
type: typeof PING_MESSAGE;
};
export type PingResponse = {
ok: boolean;
};Evaluate Expression
Evaluates a JavaScript expression in the page's main realm and returns a human-readable preview with tone metadata for syntax coloring.
export type EvaluateRequest = {
type: typeof EVALUATE_MESSAGE;
expression: string;
};
export type EvaluateResponse = {
ok: boolean;
preview: string;
tone?: ConsoleTone;
};Tone tags are mapped onto the theme's syntax colors by the Console panel, mirroring how DevTools colors primitives by type.
Console Interception
Asks the background to install a console interceptor in the page's main realm. The interceptor dispatches captured console entries on a per-injection DOM event.
export type InterceptConsoleRequest = {
type: typeof INTERCEPT_CONSOLE_MESSAGE;
eventName: string;
};
export type InterceptConsoleResponse = {
ok: boolean;
};Captured Console Data
export type CapturedConsoleMethod = "log" | "info" | "warn" | "error" | "debug";
export type CapturedConsolePayload = {
level: CapturedConsoleMethod;
text: string;
parts?: ConsolePart[];
source?: string;
};Per-argument toned segments; text remains the fallback.
file:line of the page call site, when a page frame was identifiable.
Network Interception
Installs fetch/XHR interceptors and connects them to a per-launch event channel, mirroring the console flow.
export type InterceptNetworkRequest = {
type: typeof INTERCEPT_NETWORK_MESSAGE;
eventName: string;
};
export type InterceptNetworkResponse = {
ok: boolean;
};Network Capture Phases
Network requests emit phased payloads: start, then response and body (or error). The inspector reduces them by id so a slow body read never delays the row.
export type NetworkCapturePhase = {
id: string;
phase: "start" | "response" | "body" | "error";
source?: "fetch" | "xhr";
url?: string;
method?: string;
startTime?: number;
requestHeaders?: [string, string][];
requestBody?: string | null;
requestBodyTruncated?: boolean;
status?: number;
statusText?: string;
responseHeaders?: [string, string][];
contentType?: string;
duration?: number;
responseBody?: string | null;
responseBodyTruncated?: boolean;
error?: string;
};Cookie Operations
Get Cookies
export type GetCookiesRequest = {
type: typeof GET_COOKIES_MESSAGE;
scope?: CookieScope;
};"site" lists cookies the inspector's page receives; "all" lists every cookie in the profile (requires all-sites host grant).
export type GetCookiesResponse = {
ok: boolean;
cookies: CookieEntry[];
granted?: boolean;
error?: string;
source?: CookieSource;
fallbackReason?: string;
};false when the per-site host permission has not been granted.
"document" when the response came from the page instead of the cookie store.
Why the cookie store was unreachable when the page answered instead.
Cookie Entry
export type CookieEntry = {
name: string;
value: string;
domain: string;
path: string;
expirationDate?: number;
httpOnly: boolean;
secure: boolean;
sameSite: "no_restriction" | "lax" | "strict" | "unspecified";
partial?: boolean;
};Unix seconds; absent for session cookies.
Set when a cookie was read from document.cookie rather than the cookie store. Fields below value are unknown (rendered blank) rather than defaulted.
Request Cookie Access
Prompts for the sender tab's host permission. The user's click on the panel button carries through runtime messaging as the gesture chrome.permissions.request requires.
export type RequestCookieAccessRequest = {
type: typeof REQUEST_COOKIE_ACCESS_MESSAGE;
scope?: CookieScope;
};
export type RequestCookieAccessResponse = {
ok: boolean;
error?: string;
};Delete Cookie
export type DeleteCookieRequest = {
type: typeof DELETE_COOKIE_MESSAGE;
name: string;
domain: string;
path: string;
secure: boolean;
};
export type DeleteCookieResponse = {
ok: boolean;
error?: string;
};Identifies the cookie by the fields chrome.cookies.remove needs. The background validates the domain against the sender tab.
Set Cookie
export type SetCookieRequest = {
type: typeof SET_COOKIE_MESSAGE;
original: CookieIdentity | null;
next: CookieDraft;
};
export type SetCookieResponse = {
ok: boolean;
error?: string;
};Pass null to create a new cookie, or an existing CookieIdentity to overwrite. When an edit changes the cookie's identity (name, domain, or path), the background removes the original and restores it if writing the replacement fails.
export type CookieDraft = {
name: string;
value: string;
domain: string;
path: string;
expirationDate?: number;
httpOnly: boolean;
secure: boolean;
sameSite: CookieEntry["sameSite"];
};Follows the cookie store convention: a leading dot means a domain cookie; a bare value (or "" for the tab's host) means host-only.
Clear Site Cookies
export type ClearSiteCookiesRequest = {
type: typeof CLEAR_SITE_COOKIES_MESSAGE;
};
export type ClearSiteCookiesResponse = {
ok: boolean;
removed?: number;
error?: string;
};Deletes every cookie the sender tab's page can see. Deliberately scoped to the site even when the panel lists all domains—a one-click wipe of the whole profile is a footgun DevTools does not offer.
Other Background Messages
Track Inspector
Marks the sender's tab as having the inspector open or closed. Open tabs are re-injected after reload, and their origin gets the document_start console prehook registered so capture starts before page scripts run.
export type TrackInspectorRequest = {
type: typeof TRACK_INSPECTOR_MESSAGE;
open: boolean;
activeTab?: string;
};
export type TrackInspectorResponse = {
ok: boolean;
activeTab?: string;
};Piggybacked panel-tab memory: sent when the user switches panels, it is stored per-origin. An open request without it gets the origin's last panel echoed back so a reloaded inspector reopens there.
Fetch Source
Fetches an external source file's text for the Sources panel—only sent after the user clicks the file. The panel tries a page-context fetch first; this background fallback covers cross-origin resources that reject CORS but fall under a host grant.
export type FetchSourceRequest = {
type: typeof FETCH_SOURCE_MESSAGE;
url: string;
};
export type FetchSourceResponse = {
ok: boolean;
content?: string;
truncated?: boolean;
error?: string;
};Get Network Details
Requests the webRequest header log of the sender's tab. Headers-only records cover documents, styles, images, and fonts—resources that never pass through fetch/XHR. Bodies are unavailable at this layer in MV3.
export type GetNetworkDetailsRequest = {
type: typeof GET_NETWORK_DETAILS_MESSAGE;
};
export type WebRequestEntry = {
url: string;
method: string;
resourceType: string;
status: number;
startEpoch: number;
duration: number;
fromCache: boolean;
error: string | null;
requestHeaders: [string, string][];
responseHeaders: [string, string][];
};
export type GetNetworkDetailsResponse = {
ok: boolean;
entries: WebRequestEntry[];
};Event Channels
Event names serve a dual purpose: they identify which interceptor payload should be processed, and they act as unguessable channel tokens. Page scripts that never see the event name can neither eavesdrop on captured entries nor spoof them.
Channel Token Generation
export function randomChannelToken(): string {
return typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) =>
byte.toString(16).padStart(2, "0"),
).join("");
}Uses crypto.randomUUID() when available (modern runtimes), falling back to random bytes formatted as a hexadecimal string.
Console Events
export function randomConsoleEventName(): string {
return `${CONSOLE_EVENT_PREFIX}${randomChannelToken()}`;
}
export function isConsoleEventName(value: string): boolean {
return /^inspector-lab-console:[0-9a-f-]{32,36}$/.test(value);
}The prefix is "inspector-lab-console:" followed by a 32–36 character UUID or hex token.
Network Events
export function randomNetworkEventName(): string {
return `${NETWORK_EVENT_PREFIX}${randomChannelToken()}`;
}
export function isNetworkEventName(value: string): boolean {
return /^inspector-lab-network:[0-9a-f-]{32,36}$/.test(value);
}The prefix is "inspector-lab-network:" followed by a 32–36 character UUID or hex token.
Background-side handlers use these validation functions to reject event names not generated by the inspector, preventing page scripts from tricking the background into registering listeners on arbitrary events.
Runtime Message Error Handling
The RuntimeMessageError class wraps failures that occur during message delivery or background processing.
export class RuntimeMessageError extends Error {
constructor(readonly reason: string) {
super(reason);
this.name = "RuntimeMessageError";
}
}Error Reasons
| Reason | Meaning |
|---|---|
"the extension link is gone" | Extension was reloaded or updated; content script is orphaned. |
"the background did not respond" | Reply was lost; background received the message. |
"the background did not answer in time" | Timeout exceeded; background may be suspended or slow. |
| Chrome/WebKit errors | Network or permission errors from the runtime. |
Retry Logic
Messages are retryable only when they are idempotent or read-only:
- Retryable:
GET_COOKIES,GET_NETWORK_DETAILS,FETCH_SOURCE,INTERCEPT_CONSOLE,INTERCEPT_NETWORK,PING,TRACK_INSPECTOR - Non-retryable:
EVALUATE,DELETE_COOKIE,SET_COOKIE,CLEAR_SITE_COOKIES
A retryable message that fails with "never delivered" (no connection, receiving end gone) is retried once after a 250 ms delay.
Timeout Policies
| Message | Timeout |
|---|---|
REQUEST_COOKIE_ACCESS | Indefinite (waits on user permission dialog) |
FETCH_SOURCE | 30 seconds (cold cross-origin fetch) |
| All others | 10 seconds |
Messages that wait on user interaction (REQUEST_COOKIE_ACCESS) must never timeout, or the permission dialog will be cancelled prematurely.
Runtime Detection
The messaging layer tries both chrome.runtime.sendMessage and browser.runtime.sendMessage (for Firefox and WebKit), choosing the first available namespace.
function resolveSender(): {
send: SendMessageFn;
lastError: () => string;
} | null {
const scope = globalThis as typeof globalThis & {
chrome?: typeof chrome;
browser?: typeof chrome;
};
for (const namespace of [scope.chrome, scope.browser]) {
try {
const runtime = namespace?.runtime;
if (typeof runtime?.sendMessage !== "function" || !runtime.id) continue;
return { /* ... */ };
} catch {
/* Namespace absent or throwing: try next. */
}
}