Skip to content

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 ​

ts
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)

ts
LFNetwork.stop(): Promise<NetworkState>

Stops listening

ts
LFNetwork.isListening(): boolean

Whether it is listening

ts
LFNetwork.getState(): NetworkState

Reads the listener state

ts
LFNetwork.clear(): NetworkState

Clears captured records and returns the post-clear state snapshot (reason: 'clear'; does not fire onStateChange)

ts
LFNetwork.getRequest(requestId): RequestRecord | null

Gets one request record by requestId

ts
LFNetwork.getRequests(): RequestRecord[]

Gets all captured requests

ts
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)

ts
LFNetwork.onEvent / onRequest / onResponse / onFinished / onFailed(callback): () => void

Subscribes to events (returns an unsubscribe function)

ts
LFNetwork.onStateChange(callback): () => void

Subscribes to listener state changes

Parameter details ​

start(options?) ​

OptionTypeDefaultDescription
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) ​

FieldTypeDescription
type'request' / 'response' / 'finished' / 'failed'Event kind
methodstringUnderlying CDP method (e.g. Network.responseReceived)
requestIdstringRequest id for getRequest / getResponseBody
recordNetworkRequestRecord / nullDeep-cloned snapshot of the request record at event time
rawobjectRaw 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 ​

FieldTypeDescription
requestId / method / urlstringBasics (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 / statusTextnumber / null / stringstatus is null before the response arrives
resourceTypestringCDP resource type: XHR / Fetch / Document / Script / Image / Media …
requestHeaders / responseHeadersRecord<string, string>Request/response headers
requestBodystring / nullPOST data when CDP captured it
mimeType / protocol / remoteIPAddressstringContent type / protocol (h2 …) / remote IP
servedFromCache / fromServiceWorkerbooleanWhether served from cache / a service worker
finished / failed / errorText / canceled—Completion/failure state and error text
startedAtMs / finishedAtMs / durationMsnumber / nullEpoch-ms timestamps and duration
body / bodyLoaded{ body: string, base64Encoded: boolean } / null / booleanResponse 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

js
// @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 init is 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 pass init: { status: response.status, headers: response.headers }. init.headers may be an object, a [name, value][] array or a Headers instance.
  • body may be a string, ArrayBuffer, TypedArray or DataView (binary is delivered to the page byte-for-byte); plain objects/arrays are JSON.stringify-ed; Blob / FormData / URLSearchParams / ReadableStream are not supported — they are rejected with an error in the page console and the request passes through unchanged.
  • request.body / response.body passed to callbacks are always strings; when bodyType is 'base64' they hold base64 of binary data and must not be JSON.parse-d directly.

LFRuntime.network interception example

js
// 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 },
  };
});