Skip to content

fix: wait for main world in cross-origin iframes instead of 1s timeout#2342

Open
trippyogi wants to merge 2 commits into
browserbase:mainfrom
trippyogi:fix/iframe-main-world-loading-2324
Open

fix: wait for main world in cross-origin iframes instead of 1s timeout#2342
trippyogi wants to merge 2 commits into
browserbase:mainfrom
trippyogi:fix/iframe-main-world-loading-2324

Conversation

@trippyogi

@trippyogi trippyogi commented Jul 9, 2026

Copy link
Copy Markdown

Fixes #2324.

Why

Cross-origin iframe interactions race OOPIF adoption. The <iframe> element appears in the CDP frame tree quickly, but the child document's Runtime.executionContextCreated only fires after it loads. waitForMainWorld had a hardcoded 1000ms budget (selectorResolver.ts, frame.ts), and ensureChildFrameReady was best-effort with ~1200ms, so slow-loading third-party iframes fail in about 1s with main world not ready for frame <id>, while an immediate retry succeeds in ~35ms because the execution context is cached by then. The reported ~1s failure matches the 1000ms budget exactly.

What Changed

The wait is now event-driven rather than timeout-driven:

  • executionContextRegistry.ts: waitForMainWorld waits on Runtime.executionContextCreated with a final cache check before rejecting; shared DEFAULT_MAIN_WORLD_TIMEOUT_MS = 15_000.
  • frameLocator.ts: ensureChildFrameReady now blocks until the main world is ready on the correct session, with a retry when OOPIF adoption switches sessions mid-wait.
  • selectorResolver.ts / frame.ts: use the shared constant instead of hardcoded 1000ms.

Test Plan

  • Unit: execution-context-registry.test.ts, 4 new tests covering the wait/reject/cache paths.
  • Integration: iframe-main-world-loading.spec.ts delays an iframe's load by 3s and verifies deepLocator().click() waits and succeeds instead of throwing.
  • Full unit suite run locally: 863/867 passed; 4 failures in flowlogger-eventstore.test.js are pre-existing on main at be0a2f6348716d3b5cce7247b4f85f85047a88aa.
  • Iframe-related integration specs: 9/9 locally-runnable specs passed (frame-get-location-and-click, iframe-ctx-addInitScript-race, locator-count-iframe, iframe-main-world-loading). Remaining iframe specs in the subset require external eval-site hosts that failed with ERR_SSL_PROTOCOL_ERROR in this environment — left to CI.

Design note for reviewers

The 15s ceiling means a genuinely unreachable frame now fails slower than the old 1s. Happy to wire this into an existing configurable timeout option instead if that's preferred; the constant is centralized so it's a one-line change either way.


Summary by cubic

Switch iframe main-world waiting from a fixed 1s timeout to an event-driven approach so cross-origin iframes don’t fail during slow OOPIF adoption. Fixes #2324 by waiting until the main world exists with a 15s safety net, and capping per-attempt waits to switch sessions quickly.

  • Bug Fixes
    • Made waitForMainWorld event-driven via Runtime.executionContextCreated with a final cache check; added DEFAULT_MAIN_WORLD_TIMEOUT_MS = 15_000.
    • Rewrote ensureChildFrameReady to poll session ownership during OOPIF adoption and cap per-attempt waits at 200ms.
    • Replaced hardcoded 1000ms waits in frame.ts and selectorResolver.ts with the shared constant.
    • Added unit and integration tests for delayed cross-origin iframes.

Written for commit a56940f. Summary will update on new commits.

Review in cubic

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a56940f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-evals Patch
@browserbasehq/stagehand-server-v3 Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run.
Approving the latest commit mirrors it into an internal PR owned by the approver.
If new commits are pushed later, the internal PR stays open but is marked stale until someone approves the latest external commit and refreshes it.

@github-actions github-actions Bot added external-contributor Tracks PRs mirrored from external contributor forks. external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. labels Jul 9, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.
Architecture diagram
sequenceDiagram
    participant Client as "Test / User"
    participant FrameLocator as FrameLocator
    participant Page as Page
    participant CDPSession as "Child CDPSession"
    participant ExecutionContextRegistry as ExecutionContextRegistry

    Note over Client,ExecutionContextRegistry: NEW: Event-driven wait for cross-origin iframe main world

    Client->>FrameLocator: deepLocator().click()
    FrameLocator->>FrameLocator: ensureChildFrameReady(frameId)

    loop Retry on OOPIF session change
        FrameLocator->>Page: getSessionForFrame(frameId)
        Page-->>FrameLocator: session (current owner)

        FrameLocator->>CDPSession: Runtime.enable (best-effort)
        FrameLocator->>CDPSession: Page.setLifecycleEventsEnabled (best-effort)

        FrameLocator->>ExecutionContextRegistry: waitForMainWorld(session, frameId, remaining)
        
        alt Main world already cached
            ExecutionContextRegistry-->>FrameLocator: executionContextId (immediate)
        else Event received before timeout
            par Runtime.executionContextCreated fires
                CDPSession-->>ExecutionContextRegistry: event {auxData.frameId, isDefault, context.id}
            and Page.lifecycleEvent fires
                CDPSession-->>ExecutionContextRegistry: event {frameId, name}
            end
            ExecutionContextRegistry-->>FrameLocator: executionContextId
        else Timeout expires (DEFAULT_MAIN_WORLD_TIMEOUT_MS = 15s via shared constant)
            ExecutionContextRegistry->>ExecutionContextRegistry: final cache check
            alt Found
                ExecutionContextRegistry-->>FrameLocator: executionContextId
            else Still missing
                ExecutionContextRegistry-->>FrameLocator: reject (main world not ready)
                FrameLocator->>FrameLocator: catch() -> wait 50ms, retry loop
            end
        end
    end

    FrameLocator-->>Client: resolved
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/lib/v3/understudy/frameLocator.ts Outdated
Comment thread packages/core/lib/v3/understudy/executionContextRegistry.ts Outdated
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contributor:awaiting-approval Waiting for a stagehand team member to approve the latest external commit. external-contributor Tracks PRs mirrored from external contributor forks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Clicking inside a cross-origin iframe fails with "main world not ready for frame <id>" when the iframe is still loading

1 participant