SIGN IN SIGN UP

Fix frozen pages on shared browser connections (#6279)

* Fix defaultContext publication race

The connection's recvLoop goroutine starts before connect() assigns
b.defaultContext, so an attach event could observe the field while it
is being written. Publish the assignment under contextMu so the
connection's attach filter can read it safely.

* Fix rejected browser targets staying paused

Each connection auto-attaches to every new target in the browser with
waitForDebuggerOnStart, so the browser keeps a new target paused until
every attached client releases it. When the attach filter rejected a
target, the connection sent Runtime.runIfWaitingForDebugger but stayed
attached, so the browser kept waiting on this client for future
events.

Detach from rejected targets instead. Detaching should be enough to
release the hold, but the browser has a bug where a detached target
can stay paused, so resume the target first and await the resume
response before sending the detach: the ordering must be guaranteed by
the response, not by wire order, or the browser may still process the
detach before the resume. This is the same workaround Playwright
applies in CRSession.detach (crConnection.ts). The wait is bounded
because an unresponsive target must still be detached from, or this
client would keep holding it.

Awaiting the response requires a registered session, because the
connection drops responses addressed to unknown sessions, so the
reject path now creates a session for the target before releasing it.
This also matches Playwright, where a session object exists for every
attached target. The session is cleaned up by the existing handling of
the Target.detachedFromTarget event the browser sends back.

* Fix connection filter claiming foreign targets

Each connection auto-attaches to every new target in the browser with
waitForDebuggerOnStart, so the browser keeps a new page paused until
every attached client releases it. The attach filter accepted any
target while the connection had no browser context yet, so a
connection that was still setting up claimed pages belonging to other
connections sharing the browser and never released them. Those pages
stayed frozen until the claiming connection closed: with multiple VUs
on a shared browser instance, roughly one page per browser made
progress at a time.

Only accept targets from the connection's own browser context or the
browser's default context. Foreign targets now take the reject path,
which resumes them, detaches from them, and releases the hold.

* Release targets the browser ignores

onAttachedToTarget could ignore an attached target after
isAttachedPageValid rejected it, without releasing it: the target
stayed paused, held by this client, until the connection closed.
Resume and detach from every target the browser ignores, the same
sequenced release the connection uses for filtered targets.

* Release rejected pages regardless of closing

isPageAttachmentErrorIgnorable can also return true when only this
VU's own context ended, not just when the browser is closing, but the
rejected target's session was only released in the closing case. On a
shared browser instance, the connection and its other VUs can outlive
this VU's context, so the target was left held for anyone still using
that browser.

Release the target unconditionally: detachSession is a no-op if the
connection or session is already gone, so this is safe when the
browser genuinely is closing too.

* Fix recvLoop stalling on a closed session

getSession only holds the sessions lock for the map lookup, so a
session obtained this way can already be closed by the time recvLoop
tries to deliver a message to it: closeSession is callable from any
goroutine, not just recvLoop, and detachSession's goroutine calls it
after releasing a rejected target. With no reader left on the session's
readCh once its readLoop has exited, delivering to it blocked forever,
since neither the connection's closeCh nor done fire just because one
session closed. That stalled recvLoop - the single goroutine reading
every message for the whole connection - along with every other
session sharing it.

Extract recvLoop's dispatch into deliverToSession and add a
<-session.done case, so delivering to an already-closed session
returns immediately instead of blocking. Also add a concurrency test
proving closeSession is safe to call more than once for the same
session, the same shape as detachSession's own close racing the
browser's Target.detachedFromTarget event.

* Fix detachSession leaking on a failed detach send

detachSession left a rejected target's session registered until the
browser's own Target.detachedFromTarget event arrived, but that event
is not guaranteed: the detach command itself can error against a real
browser. When it does, the session's goroutine and its entry in the
connection's sessions map are never reclaimed until the whole
connection closes.

Close the session locally right after the resume-and-detach attempt,
regardless of whether it succeeded. Safe now that a concurrent
recvLoop dispatch to the same session can no longer block.
A
Ankur committed
94169daab29df52c1ed2ee0549657a577666b855
Parent: fce5185
Committed by GitHub <noreply@github.com> on 8/21/2026, 8:34:12 AM