Skip to content

Commit b913f48

Browse files
authored
fix: cancel protocol calls on connection / driver close (microsoft#1897)
1 parent 19cf427 commit b913f48

File tree

8 files changed

+57
-28
lines changed

8 files changed

+57
-28
lines changed

playwright/_impl/_browser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from playwright._impl._cdp_session import CDPSession
3030
from playwright._impl._connection import ChannelOwner, from_channel
3131
from playwright._impl._helper import (
32+
BROWSER_CLOSED_ERROR,
3233
ColorScheme,
3334
ForcedColors,
3435
HarContentPolicy,
@@ -182,7 +183,7 @@ async def close(self) -> None:
182183
if not is_safe_close_error(e):
183184
raise e
184185
if self._should_close_connection_on_close:
185-
await self._connection.stop_async()
186+
await self._connection.stop_async(BROWSER_CLOSED_ERROR)
186187

187188
@property
188189
def version(self) -> str:

playwright/_impl/_browser_type.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from_nullable_channel,
3434
)
3535
from playwright._impl._helper import (
36+
BROWSER_CLOSED_ERROR,
3637
ColorScheme,
3738
Env,
3839
ForcedColors,
@@ -218,7 +219,7 @@ async def connect(
218219

219220
timeout_future = throw_on_timeout(timeout, Error("Connection timed out"))
220221
done, pending = await asyncio.wait(
221-
{transport.on_error_future, playwright_future, timeout_future},
222+
{playwright_future, timeout_future},
222223
return_when=asyncio.FIRST_COMPLETED,
223224
)
224225
if not playwright_future.done():
@@ -234,13 +235,13 @@ async def connect(
234235
self._did_launch_browser(browser)
235236
browser._should_close_connection_on_close = True
236237

237-
def handle_transport_close() -> None:
238+
def handle_transport_close(transport_exception: str) -> None:
238239
for context in browser.contexts:
239240
for page in context.pages:
240241
page._on_close()
241242
context._on_close()
242243
browser._on_close()
243-
connection.cleanup()
244+
connection.cleanup(transport_exception or BROWSER_CLOSED_ERROR)
244245

245246
transport.once("close", handle_transport_close)
246247

playwright/_impl/_connection.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from pyee.asyncio import AsyncIOEventEmitter
3737

3838
import playwright
39-
from playwright._impl._helper import ParsedMessagePayload, parse_error
39+
from playwright._impl._helper import Error, ParsedMessagePayload, parse_error
4040
from playwright._impl._transport import Transport
4141

4242
if TYPE_CHECKING:
@@ -242,6 +242,7 @@ def __init__(
242242
] = contextvars.ContextVar("ApiZone", default=None)
243243
self._local_utils: Optional["LocalUtils"] = local_utils
244244
self._tracing_count = 0
245+
self._closed_error_message: Optional[str] = None
245246

246247
@property
247248
def local_utils(self) -> "LocalUtils":
@@ -272,16 +273,24 @@ def stop_sync(self) -> None:
272273
self._loop.run_until_complete(self._transport.wait_until_stopped())
273274
self.cleanup()
274275

275-
async def stop_async(self) -> None:
276+
async def stop_async(self, error_message: str = None) -> None:
276277
self._transport.request_stop()
277278
await self._transport.wait_until_stopped()
278-
self.cleanup()
279+
self.cleanup(error_message)
279280

280-
def cleanup(self) -> None:
281+
def cleanup(self, error_message: str = None) -> None:
282+
if not error_message:
283+
error_message = "Connection closed"
284+
self._closed_error_message = error_message
281285
if self._init_task and not self._init_task.done():
282286
self._init_task.cancel()
283287
for ws_connection in self._child_ws_connections:
284288
ws_connection._transport.dispose()
289+
for callback in self._callbacks.values():
290+
callback.future.set_exception(Error(error_message))
291+
# Prevent 'Task exception was never retrieved'
292+
callback.future.exception()
293+
self._callbacks.clear()
285294
self.emit("close")
286295

287296
def call_on_object_with_known_name(
@@ -298,6 +307,8 @@ def set_in_tracing(self, is_tracing: bool) -> None:
298307
def _send_message_to_server(
299308
self, guid: str, method: str, params: Dict
300309
) -> ProtocolCallback:
310+
if self._closed_error_message:
311+
raise Error(self._closed_error_message)
301312
self._last_id += 1
302313
id = self._last_id
303314
callback = ProtocolCallback(self._loop)
@@ -339,6 +350,8 @@ def _send_message_to_server(
339350
return callback
340351

341352
def dispatch(self, msg: ParsedMessagePayload) -> None:
353+
if self._closed_error_message:
354+
return
342355
id = msg.get("id")
343356
if id:
344357
callback = self._callbacks.pop(id)

playwright/_impl/_helper.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,10 +314,14 @@ def prepare_interception_patterns(
314314
return patterns
315315

316316

317+
BROWSER_CLOSED_ERROR = "Browser has been closed"
318+
BROWSER_OR_CONTEXT_CLOSED_ERROR = "Target page, context or browser has been closed"
319+
320+
317321
def is_safe_close_error(error: Exception) -> bool:
318322
message = str(error)
319-
return message.endswith("Browser has been closed") or message.endswith(
320-
"Target page, context or browser has been closed"
323+
return message.endswith(BROWSER_CLOSED_ERROR) or message.endswith(
324+
BROWSER_OR_CONTEXT_CLOSED_ERROR
321325
)
322326

323327

playwright/_impl/_json_pipe.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717

1818
from pyee.asyncio import AsyncIOEventEmitter
1919

20-
from playwright._impl._api_types import Error
2120
from playwright._impl._connection import Channel
22-
from playwright._impl._helper import ParsedMessagePayload, parse_error
21+
from playwright._impl._helper import ParsedMessagePayload
2322
from playwright._impl._transport import Transport
2423

2524

@@ -37,7 +36,7 @@ def __init__(
3736

3837
def request_stop(self) -> None:
3938
self._stop_requested = True
40-
self._loop.create_task(self._pipe_channel.send("close", {}))
39+
self._pipe_channel.send_no_reply("close", {})
4140

4241
def dispose(self) -> None:
4342
self.on_error_future.cancel()
@@ -49,17 +48,17 @@ async def wait_until_stopped(self) -> None:
4948
async def connect(self) -> None:
5049
self._stopped_future: asyncio.Future = asyncio.Future()
5150

51+
close_error: Optional[str] = None
52+
5253
def handle_message(message: Dict) -> None:
53-
if not self._stop_requested:
54+
try:
5455
self.on_message(cast(ParsedMessagePayload, message))
56+
except Exception as e:
57+
nonlocal close_error
58+
close_error = str(e)
5559

56-
def handle_closed(error: Optional[Dict]) -> None:
57-
self.emit("close")
58-
self.on_error_future.set_exception(
59-
parse_error(error["error"])
60-
if error
61-
else Error("Playwright connection closed")
62-
)
60+
def handle_closed() -> None:
61+
self.emit("close", close_error)
6362
self._stopped_future.set_result(None)
6463

6564
self._pipe_channel.on(
@@ -68,13 +67,11 @@ def handle_closed(error: Optional[Dict]) -> None:
6867
)
6968
self._pipe_channel.on(
7069
"closed",
71-
lambda params: handle_closed(params.get("error")),
70+
lambda _: handle_closed(),
7271
)
7372

7473
async def run(self) -> None:
7574
await self._stopped_future
7675

7776
def send(self, message: Dict) -> None:
78-
if self._stop_requested:
79-
raise Error("Playwright connection closed")
80-
self._loop.create_task(self._pipe_channel.send("send", {"message": message}))
77+
self._pipe_channel.send_no_reply("send", {"message": message})

tests/async/test_asyncio.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
from playwright.async_api import async_playwright
2121

22+
from ..server import Server
23+
2224

2325
async def test_should_cancel_underlying_protocol_calls(
2426
browser_name: str, launch_arguments: Dict
@@ -54,3 +56,14 @@ async def test_async_playwright_stop_multiple_times() -> None:
5456
playwright = await async_playwright().start()
5557
await playwright.stop()
5658
await playwright.stop()
59+
60+
61+
async def test_cancel_pending_protocol_call_on_playwright_stop(server: Server) -> None:
62+
server.set_route("/hang", lambda _: None)
63+
playwright = await async_playwright().start()
64+
api_request_context = await playwright.request.new_context()
65+
pending_task = asyncio.create_task(api_request_context.get(server.PREFIX + "/hang"))
66+
await playwright.stop()
67+
with pytest.raises(Exception) as exc_info:
68+
await pending_task
69+
assert "Connection closed" in str(exc_info.value)

tests/async/test_browsertype_connect.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ async def test_browser_type_connect_should_throw_when_used_after_is_connected_re
145145

146146
with pytest.raises(Error) as exc_info:
147147
await page.evaluate("1 + 1")
148-
assert "Playwright connection closed" == exc_info.value.message
148+
assert "has been closed" in exc_info.value.message
149149
assert browser.is_connected() is False
150150

151151

@@ -159,7 +159,7 @@ async def test_browser_type_connect_should_reject_navigation_when_browser_closes
159159

160160
with pytest.raises(Error) as exc_info:
161161
await page.goto(server.PREFIX + "/one-style.html")
162-
assert "Playwright connection closed" in exc_info.value.message
162+
assert "has been closed" in exc_info.value.message
163163

164164

165165
async def test_should_not_allow_getting_the_path(

tests/sync/test_browsertype_connect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ def test_browser_type_connect_should_throw_when_used_after_is_connected_returns_
140140

141141
with pytest.raises(Error) as exc_info:
142142
page.evaluate("1 + 1")
143-
assert "Playwright connection closed" == exc_info.value.message
143+
assert "has been closed" in exc_info.value.message
144144
assert browser.is_connected() is False
145145

146146

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy