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