LFNetwork
Global:LFNetwork · Enable:// @require network
CDP-backed network inspection: observe requests/responses of the current tab and read response bodies. Available in all three page runtimes: saved bundles, the liveDebug USER_SCRIPT path, and run --tab MAIN-world helper scripts (the editor automatically installs an ISOLATED-world network relay bridge). Background apps (// @match background / run --target background) and run without --tab have no tab, so start() and getResponseBody() throw immediately with error.code = 'LF_NETWORK_NO_TAB'. @require dom only injects network at runtime for LFDOM's own use; calling LFNetwork directly in your source requires declaring @require network yourself.
While listening, LFNetwork attaches chrome.debugger to the tab, so the browser shows its "is debugging this browser" banner (which changes the viewport height). It calls stop() automatically when the process ends or liveDebug hot-reloads.
Methods
LFNetwork.start(options?): Promise<NetworkState>Starts listening (frameScope and event callbacks configurable); if already listening it just returns the current state and ignores the new frameScope (event callbacks are still registered)
LFNetwork.stop(): Promise<NetworkState>Stops listening
LFNetwork.isListening(): booleanWhether it is listening
LFNetwork.getState(): NetworkStateReads the listener state
LFNetwork.clear(): NetworkStateClears captured records and returns the post-clear state snapshot (reason: 'clear'; does not fire onStateChange)
LFNetwork.getRequest(requestId): RequestRecord | nullGets one request record by requestId
LFNetwork.getRequests(): RequestRecord[]Gets all captured requests
LFNetwork.getResponseBody(requestId): Promise<{ body, base64Encoded }>Reads the response body of a request (unless already cached, requires an active listener — calling it after stop() rejects)
LFNetwork.onEvent / onRequest / onResponse / onFinished / onFailed(callback): () => voidSubscribes to events (returns an unsubscribe function)
LFNetwork.onStateChange(callback): () => voidSubscribes to listener state changes
Parameter details
start(options?)
| Option | Type | Default | Description |
|---|---|---|---|
frameScope | 'page' / 'tab' | 'page' | page dispatches requests from this script's main frame and all of its child iframes (including cross-origin ones), and lets through requests with no frame (e.g. workers); the practical difference from tab is that requests from new frames not yet registered in the frame tree are conservatively dropped. Calling start() from a child iframe silently becomes 'tab'; invalid values are treated as 'page' |
onEvent / onRequest / onResponse / onFinished / onFailed | (event) => void | — | Event callbacks. Strongly prefer passing them to start() instead of registering after — the two-step pattern races against the first burst of events and drops them |
onStateChange | (state) => void | — | State-transition callback |
The most common start() failure is another debugger (e.g. DevTools) already attached to the tab — close DevTools on that tab and retry.
Event object (LFNetworkEvent)
| Field | Type | Description |
|---|---|---|
type | 'request' / 'response' / 'finished' / 'failed' | Event kind |
method | string | Underlying CDP method (e.g. Network.responseReceived) |
requestId | string | Request id for getRequest / getResponseBody |
record | NetworkRequestRecord / null | Deep-cloned snapshot of the request record at event time |
raw | object | Raw CDP params (deep clone) |
The event object itself has no url field — the URL is event.record.url. A synchronous exception thrown in a callback is only logged to the page console; it does not stop the listener and is not rethrown to the caller.
Request record (NetworkRequestRecord) common fields
| Field | Type | Description |
|---|---|---|
requestId / method / url | string | Basics (method is the name actually sent: the browser uppercases DELETE/GET/HEAD/OPTIONS/POST/PUT, while other spellings such as lowercase patch stay as-is) |
status / statusText | number / null / string | status is null before the response arrives |
resourceType | string | CDP resource type: XHR / Fetch / Document / Script / Image / Media … |
requestHeaders / responseHeaders | Record<string, string> | Request/response headers |
requestBody | string / null | POST data when CDP captured it |
mimeType / protocol / remoteIPAddress | string | Content type / protocol (h2 …) / remote IP |
servedFromCache / fromServiceWorker | boolean | Whether served from cache / a service worker |
finished / failed / errorText / canceled | — | Completion/failure state and error text |
startedAtMs / finishedAtMs / durationMs | number / null | Epoch-ms timestamps and duration |
body / bodyLoaded | { body: string, base64Encoded: boolean } / null / boolean | Response body cached after getResponseBody() was called |
Up to 1000 records are retained (oldest evicted first); getRequest / getRequests return deep clones, safe to mutate. getResponseBody(requestId) resolves { body: string, base64Encoded: boolean } — when base64Encoded is true the body is base64-encoded binary. The body is only complete after the finished event, so read it in onFinished. Bodies are best-effort and may reject (not loaded yet, evicted from the cache, or no body at all such as 204/redirects); a cached body is returned directly, otherwise an active listener is required; each command times out after 10 seconds; errors carry requestId, rawError, and a CDP error code when available. getState() returns { listening, connected, tabId, requestCount, frameScope, lastError, … }.
Example
// @require network
await LFNetwork.start({
onFinished: async (event) => {
if (!event.record || !event.record.url.includes("/api/orders")) return;
const { body, base64Encoded } = await LFNetwork.getResponseBody(event.requestId);
if (!base64Encoded) LFRuntime.print("orders payload", JSON.parse(body));
},
});LFNetwork vs LFRuntime.network
LFNetwork observes: read requests and bodies without changing page behavior. LFRuntime.network intercepts: mock or replace the page's own fetch/XHR responses. Only requests made by the page's own JS are intercepted; your app's own fetch and LFHttp calls do not pass through it. Installed apps must declare @intercept <url-pattern> in the entry header (a formal Compile checks this with the same parser the runtime uses: it is recognized even after a "use strict" prologue, and an empty value such as // @intercept "" counts as not declared); run --tab and liveDebug inject the interception host automatically, so it is optional while debugging.
Interception callbacks return { body, init? }:
- When
initis omitted the response is built with{ status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' } }, so the original status and headers are lost; to keep them passinit: { status: response.status, headers: response.headers }.init.headersmay be an object, a[name, value][]array or aHeadersinstance. bodymay be a string,ArrayBuffer, TypedArray orDataView(binary is delivered to the page byte-for-byte); plain objects/arrays areJSON.stringify-ed;Blob/FormData/URLSearchParams/ReadableStreamare not supported — they are rejected with an error in the page console and the request passes through unchanged.request.body/response.bodypassed to callbacks are always strings; whenbodyTypeis'base64'they hold base64 of binary data and must not beJSON.parse-d directly.
LFRuntime.network interception example
// Declare in the entry header: // @intercept /api/orders
LFRuntime.network.onResponse("/api/orders", (record) => {
LFRuntime.print("orders response", record.response.body);
});
LFRuntime.network.interceptResponse("/api/orders", ({ response }) => {
if (response.bodyType === "base64") return; // returning undefined keeps the original response
const data = JSON.parse(response.body);
data.items = data.items.filter((item) => item.status !== "hidden");
return {
body: JSON.stringify(data),
init: { status: response.status, headers: response.headers },
};
});