Skip to content

Commit c8d8f4a

Browse files
authored
fix: cleanup pending route handlers on close (#1412)
Fixes #1402.
1 parent 8820f30 commit c8d8f4a

10 files changed

+118
-8
lines changed

playwright/_impl/_browser_context.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from playwright._impl._frame import Frame
4949
from playwright._impl._har_router import HarRouter
5050
from playwright._impl._helper import (
51+
BackgroundTaskTracker,
5152
HarRecordingMetadata,
5253
RouteFromHarNotFoundPolicy,
5354
RouteHandler,
@@ -103,6 +104,7 @@ def __init__(
103104
self._request: APIRequestContext = from_channel(
104105
initializer["APIRequestContext"]
105106
)
107+
self._background_task_tracker: BackgroundTaskTracker = BackgroundTaskTracker()
106108
self._channel.on(
107109
"bindingCall",
108110
lambda params: self._on_binding(from_channel(params["binding"])),
@@ -113,7 +115,7 @@ def __init__(
113115
)
114116
self._channel.on(
115117
"route",
116-
lambda params: asyncio.create_task(
118+
lambda params: self._background_task_tracker.create_task(
117119
self._on_route(
118120
from_channel(params.get("route")),
119121
from_channel(params.get("request")),
@@ -163,8 +165,14 @@ def __init__(
163165
),
164166
)
165167
self._closed_future: asyncio.Future = asyncio.Future()
168+
169+
def _on_close(_: Any) -> None:
170+
self._background_task_tracker.close()
171+
self._closed_future.set_result(True)
172+
166173
self.once(
167-
self.Events.Close, lambda context: self._closed_future.set_result(True)
174+
self.Events.Close,
175+
_on_close,
168176
)
169177

170178
def __repr__(self) -> str:
@@ -187,7 +195,10 @@ async def _on_route(self, route: Route, request: Request) -> None:
187195
handled = await route_handler.handle(route, request)
188196
finally:
189197
if len(self._routes) == 0:
190-
asyncio.create_task(self._disable_interception())
198+
try:
199+
await self._disable_interception()
200+
except Exception:
201+
pass
191202
if handled:
192203
return
193204
await route._internal_continue(is_internal=True)

playwright/_impl/_helper.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,3 +362,19 @@ def is_file_payload(value: Optional[Any]) -> bool:
362362
and "mimeType" in value
363363
and "buffer" in value
364364
)
365+
366+
367+
class BackgroundTaskTracker:
368+
def __init__(self) -> None:
369+
self._pending_tasks: List[asyncio.Task] = []
370+
371+
def create_task(self, coro: Coroutine) -> asyncio.Task:
372+
task = asyncio.create_task(coro)
373+
task.add_done_callback(lambda task: self._pending_tasks.remove(task))
374+
self._pending_tasks.append(task)
375+
return task
376+
377+
def close(self) -> None:
378+
for task in self._pending_tasks:
379+
if not task.done():
380+
task.cancel()

playwright/_impl/_network.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,8 @@ def _report_handled(self, done: bool) -> None:
223223
chain = self._handling_future
224224
assert chain
225225
self._handling_future = None
226-
chain.set_result(done)
226+
if not chain.done():
227+
chain.set_result(done)
227228

228229
def _check_not_handled(self) -> None:
229230
if not self._handling_future:

playwright/_impl/_page.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from playwright._impl._frame import Frame
5656
from playwright._impl._har_router import HarRouter
5757
from playwright._impl._helper import (
58+
BackgroundTaskTracker,
5859
ColorScheme,
5960
DocumentLoadState,
6061
ForcedColors,
@@ -151,6 +152,7 @@ def __init__(
151152
self._browser_context._timeout_settings
152153
)
153154
self._video: Optional[Video] = None
155+
self._background_task_tracker = BackgroundTaskTracker()
154156
self._opener = cast("Page", from_nullable_channel(initializer.get("opener")))
155157

156158
self._channel.on(
@@ -192,7 +194,7 @@ def __init__(
192194
)
193195
self._channel.on(
194196
"route",
195-
lambda params: asyncio.create_task(
197+
lambda params: self._browser_context._background_task_tracker.create_task(
196198
self._on_route(
197199
from_channel(params["route"]), from_channel(params["request"])
198200
)
@@ -246,7 +248,10 @@ async def _on_route(self, route: Route, request: Request) -> None:
246248
handled = await route_handler.handle(route, request)
247249
finally:
248250
if len(self._routes) == 0:
249-
asyncio.create_task(self._disable_interception())
251+
try:
252+
await self._disable_interception()
253+
except Exception:
254+
pass
250255
if handled:
251256
return
252257
await self._browser_context._on_route(route, request)

tests/async/test_browsercontext_request_intercept.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,20 @@ async def test_should_give_access_to_the_intercepted_response_body(
174174
route.fulfill(response=response),
175175
eval_task,
176176
)
177+
178+
179+
async def test_should_cleanup_route_handlers_after_context_close(
180+
context: BrowserContext, page: Page
181+
) -> None:
182+
async def handle(r: Route):
183+
pass
184+
185+
await context.route("**", handle)
186+
try:
187+
await page.goto("https://example.com", timeout=700)
188+
except Exception:
189+
pass
190+
await context.close()
191+
192+
for task in asyncio.all_tasks():
193+
assert "_on_route" not in str(task)

tests/async/test_har.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ async def test_should_round_trip_har_zip(
503503
await expect(page_2.locator("body")).to_have_css(
504504
"background-color", "rgb(255, 192, 203)"
505505
)
506+
await context_2.close()
506507

507508

508509
async def test_should_round_trip_har_with_post_data(
@@ -536,6 +537,7 @@ async def test_should_round_trip_har_with_post_data(
536537
assert await page_2.evaluate(fetch_function, "3") == "3"
537538
with pytest.raises(Exception):
538539
await page_2.evaluate(fetch_function, "4")
540+
await context_2.close()
539541

540542

541543
async def test_should_disambiguate_by_header(
@@ -578,6 +580,7 @@ async def test_should_disambiguate_by_header(
578580
assert await page_2.evaluate(fetch_function, "baz2") == "baz2"
579581
assert await page_2.evaluate(fetch_function, "baz3") == "baz3"
580582
assert await page_2.evaluate(fetch_function, "baz4") == "baz1"
583+
await context_2.close()
581584

582585

583586
async def test_should_produce_extracted_zip(
@@ -605,6 +608,7 @@ async def test_should_produce_extracted_zip(
605608
await expect(page_2.locator("body")).to_have_css(
606609
"background-color", "rgb(255, 192, 203)"
607610
)
611+
await context_2.close()
608612

609613

610614
async def test_should_update_har_zip_for_context(
@@ -627,6 +631,7 @@ async def test_should_update_har_zip_for_context(
627631
await expect(page_2.locator("body")).to_have_css(
628632
"background-color", "rgb(255, 192, 203)"
629633
)
634+
await context_2.close()
630635

631636

632637
async def test_should_update_har_zip_for_page(
@@ -649,6 +654,7 @@ async def test_should_update_har_zip_for_page(
649654
await expect(page_2.locator("body")).to_have_css(
650655
"background-color", "rgb(255, 192, 203)"
651656
)
657+
await context_2.close()
652658

653659

654660
async def test_should_update_extracted_har_zip_for_page(
@@ -675,3 +681,4 @@ async def test_should_update_extracted_har_zip_for_page(
675681
await expect(page_2.locator("body")).to_have_css(
676682
"background-color", "rgb(255, 192, 203)"
677683
)
684+
await context_2.close()

tests/async/test_request_intercept.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from twisted.web import http
1919

20-
from playwright.async_api import Page, Route
20+
from playwright.async_api import BrowserContext, Page, Route
2121
from tests.server import Server
2222

2323

@@ -168,3 +168,20 @@ async def test_should_give_access_to_the_intercepted_response_body(
168168
route.fulfill(response=response),
169169
eval_task,
170170
)
171+
172+
173+
async def test_should_cleanup_route_handlers_after_context_close(
174+
context: BrowserContext, page: Page
175+
) -> None:
176+
async def handle(r: Route):
177+
pass
178+
179+
await page.route("**", handle)
180+
try:
181+
await page.goto("https://example.com", timeout=700)
182+
except Exception:
183+
pass
184+
await context.close()
185+
186+
for task in asyncio.all_tasks():
187+
assert "_on_route" not in str(task)

tests/sync/test_browsercontext_request_intercept.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import asyncio
1516
from pathlib import Path
1617

1718
from twisted.web import http
@@ -121,3 +122,17 @@ def handle_route(route: Route) -> None:
121122
assert request.uri.decode() == "/title.html"
122123
original = (assetdir / "title.html").read_text()
123124
assert response.text() == original
125+
126+
127+
def test_should_cleanup_route_handlers_after_context_close(
128+
context: BrowserContext, page: Page
129+
) -> None:
130+
context.route("**", lambda r: None)
131+
try:
132+
page.goto("https://example.com", timeout=700)
133+
except Exception:
134+
pass
135+
context.close()
136+
137+
for task in asyncio.all_tasks():
138+
assert "_on_route" not in str(task)

tests/sync/test_har.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,7 @@ def test_should_round_trip_har_with_post_data(
471471
assert page_2.evaluate(fetch_function, "3") == "3"
472472
with pytest.raises(Exception):
473473
page_2.evaluate(fetch_function, "4")
474+
context_2.close()
474475

475476

476477
def test_should_disambiguate_by_header(
@@ -512,6 +513,7 @@ def test_should_disambiguate_by_header(
512513
assert page_2.evaluate(fetch_function, "baz2") == "baz2"
513514
assert page_2.evaluate(fetch_function, "baz3") == "baz3"
514515
assert page_2.evaluate(fetch_function, "baz4") == "baz1"
516+
context_2.close()
515517

516518

517519
def test_should_produce_extracted_zip(
@@ -537,6 +539,7 @@ def test_should_produce_extracted_zip(
537539
page_2.goto(server.PREFIX + "/one-style.html")
538540
assert "hello, world!" in page_2.content()
539541
expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
542+
context_2.close()
540543

541544

542545
def test_should_update_har_zip_for_context(
@@ -557,6 +560,7 @@ def test_should_update_har_zip_for_context(
557560
page_2.goto(server.PREFIX + "/one-style.html")
558561
assert "hello, world!" in page_2.content()
559562
expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
563+
context_2.close()
560564

561565

562566
def test_should_update_har_zip_for_page(
@@ -577,6 +581,7 @@ def test_should_update_har_zip_for_page(
577581
page_2.goto(server.PREFIX + "/one-style.html")
578582
assert "hello, world!" in page_2.content()
579583
expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
584+
context_2.close()
580585

581586

582587
def test_should_update_extracted_har_zip_for_page(
@@ -601,3 +606,4 @@ def test_should_update_extracted_har_zip_for_page(
601606
page_2.goto(server.PREFIX + "/one-style.html")
602607
assert "hello, world!" in page_2.content()
603608
expect(page_2.locator("body")).to_have_css("background-color", "rgb(255, 192, 203)")
609+
context_2.close()

tests/sync/test_request_intercept.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import asyncio
1516
from pathlib import Path
1617

1718
from twisted.web import http
1819

19-
from playwright.sync_api import Page, Route
20+
from playwright.sync_api import BrowserContext, Page, Route
2021
from tests.server import Server
2122

2223

@@ -115,3 +116,17 @@ def handle_route(route: Route) -> None:
115116
assert request.uri.decode() == "/title.html"
116117
original = (assetdir / "title.html").read_text()
117118
assert response.text() == original
119+
120+
121+
def test_should_cleanup_route_handlers_after_context_close(
122+
context: BrowserContext, page: Page
123+
) -> None:
124+
page.route("**", lambda r: None)
125+
try:
126+
page.goto("https://example.com", timeout=700)
127+
except Exception:
128+
pass
129+
context.close()
130+
131+
for task in asyncio.all_tasks():
132+
assert "_on_route" not in str(task)

0 commit comments

Comments
 (0)