fix: sync Playwright loader spins a CPU core when a page opens a WebSocket (#30050)

Fetching a single URL through the Playwright loader (fetch_url tool, a URL attached to a
chat, the process/web endpoint) never returned when the page opened a WebSocket. The worker
thread stayed at 100% CPU for the life of the process, and every further hit cost another
core, so the whole instance got slow. Web search was unaffected, it uses the async loader.

The sync loader's websocket route handler called the synchronous close(). Playwright runs
websocket route handlers directly on its dispatcher fiber, so that call waited on the very
loop it was blocking and busy-spun forever. The handler is now a no-op: a routed socket only
reaches the network when the handler asks for it, so the page still cannot dial out, and
nothing in the handler waits on the dispatcher any more.

Aborting the upgrade request from the HTTP route handler instead does not work, page.route
never sees WebSocket handshakes and the connection goes through.

Fixes #30024
This commit is contained in:
Classic298 2026-09-16 04:51:37 +02:00 committed by GitHub
parent 1cdd7aa459
commit 06d9d2e7c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -868,7 +868,8 @@ class SafePlaywrightURLLoader(BaseLoader, RateLimitMixin, URLProcessingMixin):
browser.new_page(service_workers='block') as page,
):
page.route('**/*', lambda route: self._intercept_navigation_sync(route, session))
page.route_web_socket('**/*', lambda ws_route: ws_route.close())
# sync close() hangs the dispatcher; a no-op handler still never connects to the server
page.route_web_socket('**/*', lambda ws_route: None)
response = page.goto(url, timeout=self.playwright_timeout)
if response is None:
raise ValueError(f'page.goto() returned None for url {url}')