Skip to content

Commit c7e1bce

Browse files
Skn0ttCopilot
andcommitted
fix(connection): register protocol callback only after successful send
Fixes one of the two cases of #3165 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 358aa828-ed44-482a-8a83-91d435d25b38
1 parent eab2bca commit c7e1bce

3 files changed

Lines changed: 59 additions & 3 deletions

File tree

playwright/_impl/_connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,8 @@ def _send_message_to_server(
441441
if self._tracing_count > 0 and frames and object._guid != "localUtils":
442442
self.local_utils.add_stack_to_tracing_no_reply(id, frames)
443443

444-
self._callbacks[id] = callback
445444
self._transport.send(message)
445+
self._callbacks[id] = callback
446446

447447
return callback
448448

tests/async/test_asyncio.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ async def test_should_cancel_underlying_protocol_calls(
3232
) -> None:
3333
handler_exception = None
3434

35-
def exception_handlerdler(loop: asyncio.AbstractEventLoop, context: Dict) -> None:
35+
def exception_handler(loop: asyncio.AbstractEventLoop, context: Dict) -> None:
3636
nonlocal handler_exception
3737
handler_exception = context["exception"]
3838

39-
asyncio.get_running_loop().set_exception_handler(exception_handlerdler)
39+
asyncio.get_running_loop().set_exception_handler(exception_handler)
4040

4141
async with async_playwright() as p:
4242
browser = await p[browser_name].launch(**launch_arguments)
@@ -68,6 +68,24 @@ def exception_handlerdler(loop: asyncio.AbstractEventLoop, context: Dict) -> Non
6868
asyncio.get_running_loop().set_exception_handler(None)
6969

7070

71+
async def test_should_not_orphan_callback_on_non_serializable_params(
72+
browser_name: str,
73+
launch_arguments: Dict,
74+
) -> None:
75+
# Regression test for https://github.com/microsoft/playwright-python/issues/3165.
76+
# A failed transport.send must not leave a ProtocolCallback in connection._callbacks
77+
# (cleanup would later set_exception on it → "Future exception was never retrieved").
78+
async with async_playwright() as p:
79+
browser = await p[browser_name].launch(**launch_arguments)
80+
page = await browser.new_page()
81+
connection = page._impl_obj._connection
82+
before = set(connection._callbacks)
83+
with pytest.raises(TypeError, match="JSON serializable"):
84+
await page.locator("asdf").highlight(style=object()) # type: ignore
85+
assert set(connection._callbacks) == before
86+
await browser.close()
87+
88+
7189
async def test_async_playwright_stop_multiple_times() -> None:
7290
playwright = await async_playwright().start()
7391
await playwright.stop()

tests/sync/test_sync.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414

1515
import multiprocessing
1616
import os
17+
import subprocess
18+
import sys
19+
import textwrap
1720
from datetime import timedelta
21+
from pathlib import Path
1822
from typing import Any, Callable, Dict
1923

2024
import pytest
@@ -361,6 +365,40 @@ def test_should_return_proper_api_name_on_error(page: Page) -> None:
361365
assert str(error).startswith("Page.evaluate:")
362366

363367

368+
def test_should_not_orphan_callback_on_non_serializable_params(
369+
browser_name: str,
370+
launch_arguments: Dict[str, Any],
371+
tmp_path: Path,
372+
) -> None:
373+
# Regression test for https://github.com/microsoft/playwright-python/issues/3165.
374+
# Run in a subprocess so "Future exception was never retrieved" on exit is visible on stderr.
375+
script = tmp_path / "orphan_callback.py"
376+
script.write_text(
377+
textwrap.dedent(
378+
f"""
379+
from playwright.sync_api import sync_playwright
380+
381+
with sync_playwright() as p:
382+
browser = p[{browser_name!r}].launch(**{launch_arguments!r})
383+
page = browser.new_page()
384+
try:
385+
page.locator("asdf").highlight(style=object())
386+
except TypeError:
387+
pass
388+
browser.close()
389+
"""
390+
)
391+
)
392+
result = subprocess.run(
393+
[sys.executable, str(script)],
394+
capture_output=True,
395+
text=True,
396+
timeout=60,
397+
)
398+
assert result.returncode == 0, result.stderr
399+
assert "Future exception was never retrieved" not in result.stderr
400+
401+
364402
def test_click_should_accept_timedelta_for_timeout(page: Page) -> None:
365403
with pytest.raises(TimeoutError, match="Timeout 1ms exceeded"):
366404
page.click("does-not-exist", timeout=timedelta(milliseconds=1))

0 commit comments

Comments
 (0)