Skip to content

CDP: Page.createIsolatedWorld(childFrameId) evaluates the main document #3347

Description

@viktor-shcherb

Summary

When Page.createIsolatedWorld is called with the ID of a loaded cross-origin child frame, Lightpanda returns an execution-context ID that evaluates against the main document, not the requested child document.

The behavior reproduces on both the latest stable release (0.3.7) and the current nightly (1.0.0-nightly.9021+27831c20b). Chromium evaluates the correct child document with the same CDP sequence.

The reproduction uses two local HTTP fixture servers on different ports. It makes no requests to third-party websites.

Environment

Component Version tested
Lightpanda stable 0.3.7, tag commit 5b0835f5a17fbb6fd45b56e091a14dded2e887b0
Stable Docker image linux/amd64, lightpanda/browser@sha256:eae2c7f8ff39a7a24540d21c7561ce219fd14b1bf2b92403930c6d140f2d1337
Lightpanda nightly 1.0.0-nightly.9021+27831c20b, commit 27831c20bf50bfd3220a89643e85e192df737ca2
Nightly Docker image Created 2026-08-30T02:50:37Z, linux/amd64, lightpanda/browser@sha256:d22c9cc1df1e6f081d90deb0fee96938014262f49406c547cda772982229103a
Chromium control Playwright 1.62.0 browser: Chrome for Testing 151.0.7922.34, macOS arm64 binary SHA-256 7687bff7cb2db075f250e6d5848bbc8838cac3802ac3952a899c574f8eccab45
CDP client github.com/chromedp/chromedp v0.14.2
Go used for the run go1.27.0 darwin/arm64
Container runtime Docker Desktop engine 28.3.2

Nightly disables iframe resource loading by default, so its command below includes --load-resources iframe. Without that flag, the missing child frame is expected and is not the bug described here.

Reproduction

The complete self-contained Go reproducer, dependency manifest, and checksum file are available in this public Gist:

https://gist.github.com/viktor-shcherb/cd1175056d7ac4d755e29d4a21ab6576

git clone https://gist.github.com/cd1175056d7ac4d755e29d4a21ab6576.git lightpanda-iframe-repro
cd lightpanda-iframe-repro
go mod verify

The fixture creates:

  • a parent document titled Parent jobs;
  • a cross-origin iframe initially titled Jobs page one; and
  • a second iframe document titled Jobs page two.

The relevant CDP sequence is:

frameTree, err := page.GetFrameTree().Do(ctx)
if err != nil {
    return err
}

mainFrameID := frameTree.Frame.ID
childFrameID := frameTree.ChildFrames[0].Frame.ID

mainContextID, err := page.CreateIsolatedWorld(mainFrameID).
    WithWorldName("repro-main-utility").
    Do(ctx)
if err != nil {
    return err
}

childContextID, err := page.CreateIsolatedWorld(childFrameID).
    WithWorldName("repro-child-utility").
    Do(ctx)
if err != nil {
    return err
}

childTitle, exception, err := runtime.Evaluate("document.title").
    WithContextID(childContextID).
    WithReturnByValue(true).
    Do(ctx)

The program evaluates the child title, changes the iframe from /frame-one to /frame-two, obtains the navigated child frame, creates a new isolated world for it, and evaluates its title again. It also evaluates through the original main-frame context to verify that child navigation did not retarget or invalidate the main world.

Latest stable Lightpanda

docker run --rm --detach \
  --platform linux/amd64 \
  --name lightpanda-iframe-repro \
  --publish 127.0.0.1:9222:9222 \
  lightpanda/browser@sha256:eae2c7f8ff39a7a24540d21c7561ce219fd14b1bf2b92403930c6d140f2d1337

go run . \
  -backend lightpanda-0.3.7 \
  -cdp ws://127.0.0.1:9222 \
  -parent-listen 0.0.0.0:18080 \
  -child-listen 0.0.0.0:18081 \
  -parent-url http://host.docker.internal:18080/ \
  -child-origin http://host.docker.internal:18081

Current nightly

docker run --rm --detach \
  --platform linux/amd64 \
  --name lightpanda-iframe-repro \
  --publish 127.0.0.1:9222:9222 \
  lightpanda/browser@sha256:d22c9cc1df1e6f081d90deb0fee96938014262f49406c547cda772982229103a \
  /bin/lightpanda serve \
  --host 0.0.0.0 \
  --port 9222 \
  --load-resources iframe

go run . \
  -backend lightpanda-nightly-27831c20b \
  -cdp ws://127.0.0.1:9222 \
  -parent-listen 0.0.0.0:18080 \
  -child-listen 0.0.0.0:18081 \
  -parent-url http://host.docker.internal:18080/ \
  -child-origin http://host.docker.internal:18081

Chromium control

"$CHROMIUM_BIN" \
  --headless \
  --no-sandbox \
  --remote-debugging-address=127.0.0.1 \
  --remote-debugging-port=9223 \
  --user-data-dir="$CHROMIUM_PROFILE" \
  about:blank

CHROMIUM_WS="$(curl --silent http://127.0.0.1:9223/json/version | jq -r .webSocketDebuggerUrl)"

go run . \
  -backend chromium-151.0.7922.34 \
  -cdp "$CHROMIUM_WS" \
  -parent-listen 127.0.0.1:18080 \
  -child-listen 127.0.0.1:18081 \
  -parent-url http://127.0.0.1:18080/ \
  -child-origin http://127.0.0.1:18081

Expected behavior

  • The main-frame world evaluates Parent jobs.
  • The initial child-frame world evaluates Jobs page one.
  • After child navigation, a newly created child-frame world evaluates Jobs page two.
  • Child navigation does not invalidate or retarget the saved main-frame world.

Chromium produces:

{
  "backend": "chromium-151.0.7922.34",
  "title_before_child_navigate": "Parent jobs",
  "title_after_child_navigate": "Parent jobs",
  "child_title_before_navigate": "Jobs page one",
  "child_title_after_navigate": "Jobs page two"
}

Actual behavior

Both tested Lightpanda builds return the parent title from execution contexts requested for the child frame.

Lightpanda 0.3.7:

{
  "backend": "lightpanda-0.3.7",
  "title_before_child_navigate": "Parent jobs",
  "title_after_child_navigate": "Parent jobs",
  "child_title_before_navigate": "Parent jobs",
  "child_title_after_navigate": "Parent jobs"
}

Nightly 27831c20b, with iframe loading enabled, produces the same result:

{
  "backend": "lightpanda-nightly-27831c20b",
  "title_before_child_navigate": "Parent jobs",
  "title_after_child_navigate": "Parent jobs",
  "child_title_before_navigate": "Parent jobs",
  "child_title_after_navigate": "Parent jobs"
}

The saved main-frame execution context remains valid. The incorrect behavior is specifically that the child-frame contexts resolve to the main document.

Downstream usage context: Jobseek

Jobseek is an open-source crawler that monitors company career pages. It is migrating its crawler runtime from Python + Playwright/Chromium to Go + self-hosted Lightpanda. Browser capabilities that Lightpanda cannot execute remain assigned to the isolated Chromium compatibility backend.

This child-frame functionality is needed by Jobseek's rendered DOM monitor:

  • The hexagon-robotics-careers board configuration identifies an Onlyfy iframe and an in-frame pagination selector.
  • The _execute_repeat implementation evaluates link counts, dispatches the pagination click, and extracts resulting job links inside the child frame.
  • The extracted iframe links are injected into the main document so the ordinary DOM link-discovery pipeline can process them.

The concrete board embeds an Onlyfy job list and requires repeated clicks on its in-frame pagination control:

{
  "page": "https://robotics.hexagon.com/careers/",
  "frame": "iframe[src*=\"onlyfy\"]",
  "action": "repeat-click",
  "selector": "a.infinite-next"
}

The automation must count links, click a.infinite-next, and extract the resulting links inside the child frame. When the child execution context points to the parent document, the selector is evaluated in the wrong DOM and the interaction cannot be completed. Disabling subframes is not a workaround because the required content exists only inside the iframe.

Until child-frame isolated worlds target the correct document, this configuration must continue using Chromium rather than Lightpanda. That leaves a concrete Chromium-only capability in Jobseek's migration instead of allowing the shared browser executor to route the board to Lightpanda.

Suggested acceptance test

Add a CDP integration fixture with one main frame and one cross-origin iframe and assert that:

  1. Page.createIsolatedWorld returns independently usable contexts for the main and child frames;
  2. each context evaluates the correct document.title;
  3. navigating only the child allows a new child world to evaluate the new child document;
  4. the original main world still evaluates the main document; and
  5. the behavior works through direct CDP and drivers that use isolated utility worlds.

Potentially related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions