Skip to content

[Bug]: Python sync API logs CancelledError when exiting sync_playwright() with an unanswered route #3272

Open

Description

@jbarker-uwcu

Version

1.64.0 (latest python). Also reproduces on 1.63.0.

Steps to reproduce

Save as repro.py and run python repro.py.

import logging
from playwright.sync_api import sync_playwright

logging.basicConfig(level=logging.ERROR)
held = []
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.route("**/held", lambda route: held.append(route))  # hold, never answer
    page.set_content("<p>x</p>")
    page.evaluate("() => { fetch('http://example.test/held').catch(() => {}); }")
    page.wait_for_timeout(500)
    browser.close()
# Exiting the with block cancels pending tasks in Playwright's own event loop.

Expected behavior

A route handler task that is still pending when exiting with sync_playwright() gets cancelled without an error being logged. Cancelling pending tasks during context-manager shutdown is the behavior microsoft/playwright-python#1430 introduced while fixing microsoft/playwright-python#1402.

Actual behavior

During context-manager shutdown, asyncio logs an error from Playwright's own done callback:

ERROR:asyncio:Exception in callback Connection.dispatch.<locals>._done_callback(<Task cancell..._page.py:298>>) at .../playwright/_impl/_connection.py:546
handle: <Handle Connection.dispatch.<locals>._done_callback(<Task cancell..._page.py:298>>) at .../playwright/_impl/_connection.py:546>
Traceback (most recent call last):
  File "/usr/lib/python3.12/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
  File ".../playwright/_impl/_connection.py", line 547, in _done_callback
    exc = future.exception()
          ^^^^^^^^^^^^^^^^^^
  File ".../playwright/_impl/_page.py", line 312, in _on_route
    handled = await route_handler.handle(route)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../playwright/_impl/_helper.py", line 443, in handle
    return await self._handle_internal(route)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../playwright/_impl/_helper.py", line 485, in _handle_internal
    return await handled_future
           ^^^^^^^^^^^^^^^^^^^^
asyncio.exceptions.CancelledError

Additional context

  • Cause: _done_callback in Connection.dispatch (sync branch only) calls future.exception() without checking future.cancelled() first. On a cancelled future, Future.exception() raises CancelledError instead of returning it, so the callback raises and asyncio reports it. The affected code is in the Python binding's playwright/_impl/_connection.py.

  • Shutdown timing: PlaywrightContextManager.__exit__ cancels pending tasks before closing the event loop it owns. In this reproduction, that happens when leaving the with block after browser.close(), rather than necessarily inside browser.close() itself.

  • Fix: an early return for a cancelled future prevents the callback from raising. With this change the repro ran clean 5 times under python -X dev:

    def _done_callback(future: asyncio.Future) -> None:
        if future.cancelled():
            return
        exc = future.exception()
        ...

Environment

- Operating System: Ubuntu 24.04.3 LTS (Linux 6.8.0-101-generic x86_64)
- CPU: x86_64, 8 cores
- Browser: Chromium 156.0.8078.4 (bundled with playwright 1.64.0, headless)
- Python Version: 3.12.3
- Other info: sync API

Also repro'd on Windows:
- Windows, Python 3.12.10
- Playwright 1.64.0
- Bundled headless Chromium 156.0.8078.4

Activity

  1. dcrousso commented on Oct 21, 2026

    @dcrousso

    Hi, the Playwright bot looked into this on my behalf and reproduced it locally.

    Reproduced in 1.63.0 and 1.64.0, already fixed on main, not yet released. The client replacement in #3280 handles the cancelled route task without producing this callback error. The latest release, 1.64.0, remains affected.

    Reproduction

    The bot extended the existing owned-loop shutdown test, using its local test server rather than an external URL. An ERROR-level handler on the asyncio logger captured the output. This is the core of the test extension, where empty_page comes from the local server and output is the captured log stream:

    page.goto(empty_page)
    held = []
    page.route("**/held", lambda route: held.append(route))
    page.evaluate(
        """() => {
            window.heldSettled = false;
            fetch('/held').finally(() => { window.heldSettled = true; }).catch(() => {});
        }"""
    )
    deadline = time.monotonic() + 5
    while not held and time.monotonic() < deadline:
        page.wait_for_timeout(10)
    assert len(held) == 1
    assert page.evaluate("window.heldSettled") is False
    browser.close()
    playwright.stop()
    assert output.getvalue() == "", output.getvalue()

    The final assertion fails in both released versions with Connection.dispatch.<locals>._done_callback and asyncio.exceptions.CancelledError.

    What the bot checked

    All checks ran on macOS 27.0.1 arm64 with Python 3.14.6. Every run confirmed that one route was held and its fetch remained unsettled before shutdown.

    Client Chromium / Firefox / WebKit Mode Result
    1.63.0 153.0.8010.12 / 155.0 / 26.6 Headless All three log CancelledError
    1.64.0 156.0.8078.4 / 157.0 / 27.2 Headless All three log CancelledError
    Fix parent 4e1ce7ad72ddd538dff046a0bc1b61c825574c43 156.0.8078.12 / 157.0 / 27.2 Headless All three log CancelledError
    Fix commit 36d698e1776181c8c7d31bc9fdc32af072cab081 156.0.8078.12 / 157.0 / 27.2 Headless All three quiet
    Main c4ee1239d2662011275d79d62e844d5f9c19bd38 156.0.8078.12 / 157.0 / 27.2 Headless and headed All six quiet

    The fix parent and fix commit used the same Node driver and browsers, isolating the change to the Python client. No release tag contains the fix yet. Linux, Windows and Python 3.12 were not checked locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions