Skip to content

fix(browser): the CDP relay passes each answer on in order with the events around it - #628

Merged
earakely-scale merged 1 commit into
mainfrom
edgararakelyan/relay-answer-order
Nov 12, 2026
Merged

earakely-scale merged 1 commit into
mainfrom
edgararakelyan/relay-answer-order

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The browser's CDP relay could deliver an event before the answer that the browser sent ahead of it. When that happened to Page.getFrameTree's answer, Playwright dropped the page's Runtime.executionContextCreated, and page.evaluate then waited for ever. The relay now passes on each answer and each event in the order the browser sent them.

It was found while testing #615. There, Playwright MCP's browser_evaluate hung now and then in a new session. It can affect any CDP client through the relay, Browser Use included.

Why it happened

The relay's pipe reader read one message at a time and handled each according to its kind:

  • An event went to the relay's listener at once, which queued it for its client.
  • An answer resolved the waiting command's future, and reached the client only once that command's task resumed.

When the browser's answer and the events after it arrived in one read, the reader handed the events on before the command's task ran. So the client got them first.

The browser sends Page.getFrameTree's answer before the execution contexts that Runtime.enable reports. Playwright, getting a context for a frame it doesn't know yet, ignores it, and so it never learns the page's main world.

This goes back to the relay's start (#482). #613 wrapped each request in a task of its own, which added a hop before the reply and widened the window.

What changes

  • DevToolsPipe.request takes an answered hook. The reader calls it with the answer as it reads it, in order with the events before and after it, then resolves the request as before.
  • The relay sends its reply to the client from that hook. Replies it makes itself (a refusal, the guard's answer, an error once the pipe closes) go as before.
  • Nothing else calls request with the hook, so other callers are unchanged.

Test plan

  • Unit:

    • test_devtools_pipe.py: the real pipe over OS pipes, with an answer and an event written in one write. The hook hears the answer before the listener hears the event.
    • test_browser_cdp.py: an answer reaches its client before the event the browser sent right after it.
    • The protocol package: 1,467 passed, 1 skipped.
  • A stress loop on a browser computer, local Docker (Mac, arm64), on feat(drivers): the browser computer serves Playwright MCP's browser_* tools, attached over CDP (FD-4955) #615's branch, whose Playwright MCP opens a CDP connection per session. Each round calls browser_evaluate in a new session.

    Without the fix With it
    Evaluate alone hung at round 5 40 of 40, each within 0.6 s
    After a dialog hung at rounds 2 and 16 40 of 40, each within 0.6 s

    In each hang the browser's log showed every command answered, and the evaluate never sent.

Docs

None: the relay's contract is unchanged. It now keeps the browser's order, as a client already assumed.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no actionable issues were found.

Summary

The relay now queues browser replies from the pipe reader, preserving their order with nearby events.

  • Browser CDP clients receive each answer before later events.

Diagram

sequenceDiagram
    participant Browser
    participant Pipe as DevToolsPipe
    participant Relay
    participant Client
    Browser->>Pipe: Command answer
    Pipe->>Relay: answered callback
    Relay->>Relay: Queue client reply
    Pipe->>Pipe: Resolve waiting request
    Browser->>Pipe: Following event
    Pipe->>Relay: Event listener
    Relay->>Relay: Queue event
    Relay->>Client: Reply, then event
Loading

Reviews (1) · Last reviewed commit: "fix(browser): the CDP relay passes each ..." · Reviewed by Greptile

…vents around it

The pipe's reader handed events to the relay as it read them, but an answer
only reached its client once the command's task resumed, so an event the
browser sent after an answer, read in the same chunk, could reach the client
first. Playwright, given Runtime.executionContextCreated before
Page.getFrameTree's answer, drops the context, and page.evaluate then waits
for ever.

A request's answered hook now hears its answer as the pipe reads it, and the
relay sends its reply from there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner November 11, 2026 23:32
@earakely-scale
earakely-scale merged commit 23e6cc9 into main Nov 12, 2026
35 checks passed
@earakely-scale
earakely-scale deleted the edgararakelyan/relay-answer-order branch November 12, 2026 02:36
earakely-scale added a commit that referenced this pull request Nov 12, 2026
…628)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant