Skip to content

Commit 040dc89

Browse files
committed
more
1 parent ac3211e commit 040dc89

File tree

8 files changed

+661
-39
lines changed

8 files changed

+661
-39
lines changed

playwright/_impl/_browser_context.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def __init__(
196196
self.Events.Close, lambda context: self._closed_future.set_result(True)
197197
)
198198
self._close_reason: Optional[str] = None
199+
self._har_routers: List[HarRouter] = []
199200
self._set_event_to_subscription_mapping(
200201
{
201202
BrowserContext.Events.Console: "console",
@@ -219,8 +220,12 @@ def _on_page(self, page: Page) -> None:
219220

220221
async def _on_route(self, route: Route) -> None:
221222
route._context = self
223+
page = route.request._safe_page()
222224
route_handlers = self._routes.copy()
223225
for route_handler in route_handlers:
226+
# If the page or the context was closed we stall all requests right away.
227+
if (page and page._close_was_called) or self._close_was_called:
228+
return
224229
if not route_handler.matches(route.request.url):
225230
continue
226231
if route_handler not in self._routes:
@@ -238,7 +243,12 @@ async def _on_route(self, route: Route) -> None:
238243
)
239244
if handled:
240245
return
241-
await route._internal_continue(is_internal=True)
246+
try:
247+
# If the page is closed or unrouteAll() was called without waiting and interception disabled,
248+
# the method will throw an error - silence it.
249+
await route._internal_continue(is_internal=True)
250+
except Exception:
251+
pass
242252

243253
def _on_binding(self, binding_call: BindingCall) -> None:
244254
func = self._bindings.get(binding_call._initializer["name"])
@@ -363,18 +373,37 @@ async def route(
363373
async def unroute(
364374
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None
365375
) -> None:
366-
self._routes = list(
367-
filter(
368-
lambda r: r.matcher.match != url or (handler and r.handler != handler),
369-
self._routes,
370-
)
371-
)
376+
removed = []
377+
remaining = []
378+
for route in self._routes:
379+
if route.matcher.match != url or (handler and route.handler != handler):
380+
remaining.append(route)
381+
else:
382+
removed.append(route)
383+
await self._unroute_internal(removed, remaining, "default")
384+
385+
async def _unroute_internal(
386+
self,
387+
removed: List[RouteHandler],
388+
remaining: List[RouteHandler],
389+
behavior: Literal["default", "ignoreErrors", "wait"] = None,
390+
) -> None:
391+
self._routes = remaining
372392
await self._update_interception_patterns()
393+
if behavior is None or behavior == "default":
394+
return
395+
await asyncio.gather(*map(lambda router: router.stop(behavior), removed)) # type: ignore
396+
397+
def _dispose_har_routers(self) -> None:
398+
for router in self._har_routers:
399+
router.dispose()
400+
self._har_routers = []
373401

374402
async def unroute_all(
375403
self, behavior: Literal["default", "ignoreErrors", "wait"] = None
376404
) -> None:
377-
pass
405+
await self._unroute_internal(self._routes, [], behavior)
406+
self._dispose_har_routers()
378407

379408
async def _record_into_har(
380409
self,
@@ -426,6 +455,7 @@ async def route_from_har(
426455
not_found_action=notFound or "abort",
427456
url_matcher=url,
428457
)
458+
self._har_routers.append(router)
429459
await router.add_context_route(self)
430460

431461
async def _update_interception_patterns(self) -> None:
@@ -457,6 +487,7 @@ def _on_close(self) -> None:
457487
if self._browser:
458488
self._browser._contexts.remove(self)
459489

490+
self._dispose_har_routers()
460491
self.emit(BrowserContext.Events.Close, self)
461492

462493
async def close(self, reason: str = None) -> None:

playwright/_impl/_har_router.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,14 @@ async def add_context_route(self, context: "BrowserContext") -> None:
102102
url=self._options_url_match or "**/*",
103103
handler=lambda route, _: asyncio.create_task(self._handle(route)),
104104
)
105-
context.once("close", lambda _: self._dispose())
106105

107106
async def add_page_route(self, page: "Page") -> None:
108107
await page.route(
109108
url=self._options_url_match or "**/*",
110109
handler=lambda route, _: asyncio.create_task(self._handle(route)),
111110
)
112-
page.once("close", lambda _: self._dispose())
113111

114-
def _dispose(self) -> None:
112+
def dispose(self) -> None:
115113
asyncio.create_task(
116114
self._local_utils._channel.send("harClose", {"harId": self._har_id})
117115
)

playwright/_impl/_helper.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
List,
3232
Optional,
3333
Pattern,
34+
Set,
3435
TypeVar,
3536
Union,
3637
cast,
@@ -257,6 +258,15 @@ def monotonic_time() -> int:
257258
return math.floor(time.monotonic() * 1000)
258259

259260

261+
class RouteHandlerInvocation:
262+
complete: "asyncio.Future"
263+
route: "Route"
264+
265+
def __init__(self, complete: "asyncio.Future", route: "Route") -> None:
266+
self.complete = complete
267+
self.route = route
268+
269+
260270
class RouteHandler:
261271
def __init__(
262272
self,
@@ -270,11 +280,29 @@ def __init__(
270280
self._times = times if times else math.inf
271281
self._handled_count = 0
272282
self._is_sync = is_sync
283+
self._ignore_exception = False
284+
self._active_invocations: Set[RouteHandlerInvocation] = set()
273285

274286
def matches(self, request_url: str) -> bool:
275287
return self.matcher.matches(request_url)
276288

277289
async def handle(self, route: "Route") -> bool:
290+
handler_invocation = RouteHandlerInvocation(
291+
asyncio.get_running_loop().create_future(), route
292+
)
293+
self._active_invocations.add(handler_invocation)
294+
try:
295+
return await self._handle_internal(route)
296+
except Exception as e:
297+
# If the handler was stopped (without waiting for completion), we ignore all exceptions.
298+
if self._ignore_exception:
299+
return False
300+
raise e
301+
finally:
302+
handler_invocation.complete.set_result(None)
303+
self._active_invocations.remove(handler_invocation)
304+
305+
async def _handle_internal(self, route: "Route") -> bool:
278306
handled_future = route._start_handling()
279307
handler_task = []
280308

@@ -297,6 +325,20 @@ def impl() -> None:
297325
[handled, *_] = await asyncio.gather(handled_future, *handler_task)
298326
return handled
299327

328+
async def stop(self, behavior: Literal["ignoreErrors", "wait"]) -> None:
329+
# When a handler is manually unrouted or its page/context is closed we either
330+
# - wait for the current handler invocations to finish
331+
# - or do not wait, if the user opted out of it, but swallow all exceptions
332+
# that happen after the unroute/close.
333+
if behavior == "ignoreErrors":
334+
self._ignore_exception = True
335+
else:
336+
tasks = []
337+
for activation in self._active_invocations:
338+
if not activation.route._did_throw:
339+
tasks.append(activation.complete)
340+
await asyncio.gather(*tasks)
341+
300342
@property
301343
def will_expire(self) -> bool:
302344
return self._handled_count + 1 >= self._times

playwright/_impl/_network.py

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ def _target_closed_future(self) -> asyncio.Future:
267267
return asyncio.Future()
268268
return page._closed_or_crashed_future
269269

270+
def _safe_page(self) -> "Optional[Page]":
271+
return cast("Frame", from_channel(self._initializer["frame"]))._page
272+
270273

271274
class Route(ChannelOwner):
272275
def __init__(
@@ -275,6 +278,7 @@ def __init__(
275278
super().__init__(parent, type, guid, initializer)
276279
self._handling_future: Optional[asyncio.Future["bool"]] = None
277280
self._context: "BrowserContext" = cast("BrowserContext", None)
281+
self._did_throw = False
278282

279283
def _start_handling(self) -> "asyncio.Future[bool]":
280284
self._handling_future = asyncio.Future()
@@ -298,17 +302,17 @@ def request(self) -> Request:
298302
return from_channel(self._initializer["request"])
299303

300304
async def abort(self, errorCode: str = None) -> None:
301-
self._check_not_handled()
302-
await self._race_with_page_close(
303-
self._channel.send(
304-
"abort",
305-
{
306-
"errorCode": errorCode,
307-
"requestUrl": self.request._initializer["url"],
308-
},
305+
await self._handle_route(
306+
lambda: self._race_with_page_close(
307+
self._channel.send(
308+
"abort",
309+
{
310+
"errorCode": errorCode,
311+
"requestUrl": self.request._initializer["url"],
312+
},
313+
)
309314
)
310315
)
311-
self._report_handled(True)
312316

313317
async def fulfill(
314318
self,
@@ -320,7 +324,22 @@ async def fulfill(
320324
contentType: str = None,
321325
response: "APIResponse" = None,
322326
) -> None:
323-
self._check_not_handled()
327+
await self._handle_route(
328+
lambda: self._inner_fulfill(
329+
status, headers, body, json, path, contentType, response
330+
)
331+
)
332+
333+
async def _inner_fulfill(
334+
self,
335+
status: int = None,
336+
headers: Dict[str, str] = None,
337+
body: Union[str, bytes] = None,
338+
json: Any = None,
339+
path: Union[str, Path] = None,
340+
contentType: str = None,
341+
response: "APIResponse" = None,
342+
) -> None:
324343
params = locals_to_params(locals())
325344

326345
if json is not None:
@@ -375,7 +394,15 @@ async def fulfill(
375394
params["requestUrl"] = self.request._initializer["url"]
376395

377396
await self._race_with_page_close(self._channel.send("fulfill", params))
378-
self._report_handled(True)
397+
398+
async def _handle_route(self, callback: Callable) -> None:
399+
self._check_not_handled()
400+
try:
401+
await callback()
402+
self._report_handled(True)
403+
except Exception as e:
404+
self._did_throw = True
405+
raise e
379406

380407
async def fetch(
381408
self,
@@ -418,10 +445,12 @@ async def continue_(
418445
postData: Union[Any, str, bytes] = None,
419446
) -> None:
420447
overrides = cast(FallbackOverrideParameters, locals_to_params(locals()))
421-
self._check_not_handled()
422-
self.request._apply_fallback_overrides(overrides)
423-
await self._internal_continue()
424-
self._report_handled(True)
448+
449+
async def _inner() -> None:
450+
self.request._apply_fallback_overrides(overrides)
451+
await self._internal_continue()
452+
453+
return await self._handle_route(_inner)
425454

426455
def _internal_continue(
427456
self, is_internal: bool = False
@@ -458,11 +487,11 @@ async def continue_route() -> None:
458487
return continue_route()
459488

460489
async def _redirected_navigation_request(self, url: str) -> None:
461-
self._check_not_handled()
462-
await self._race_with_page_close(
463-
self._channel.send("redirectNavigationRequest", {"url": url})
490+
await self._handle_route(
491+
lambda: self._race_with_page_close(
492+
self._channel.send("redirectNavigationRequest", {"url": url})
493+
)
464494
)
465-
self._report_handled(True)
466495

467496
async def _race_with_page_close(self, future: Coroutine) -> None:
468497
fut = asyncio.create_task(future)

playwright/_impl/_page.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ def __init__(
152152
self._video: Optional[Video] = None
153153
self._opener = cast("Page", from_nullable_channel(initializer.get("opener")))
154154
self._close_reason: Optional[str] = None
155+
self._close_was_called = False
156+
self._har_routers: List[HarRouter] = []
155157

156158
self._channel.on(
157159
"bindingCall",
@@ -238,6 +240,9 @@ async def _on_route(self, route: Route) -> None:
238240
route._context = self.context
239241
route_handlers = self._routes.copy()
240242
for route_handler in route_handlers:
243+
# If the page was closed we stall all requests right away.
244+
if self._close_was_called or self.context._close_was_called:
245+
return
241246
if not route_handler.matches(route.request.url):
242247
continue
243248
if route_handler not in self._routes:
@@ -274,6 +279,7 @@ def _on_close(self) -> None:
274279
self._browser_context._pages.remove(self)
275280
if self in self._browser_context._background_pages:
276281
self._browser_context._background_pages.remove(self)
282+
self._dispose_har_routers()
277283
self.emit(Page.Events.Close, self)
278284

279285
def _on_crash(self) -> None:
@@ -587,18 +593,42 @@ async def route(
587593
async def unroute(
588594
self, url: URLMatch, handler: Optional[RouteHandlerCallback] = None
589595
) -> None:
590-
self._routes = list(
591-
filter(
592-
lambda r: r.matcher.match != url or (handler and r.handler != handler),
593-
self._routes,
596+
removed = []
597+
remaining = []
598+
for route in self._routes:
599+
if route.matcher.match != url or (handler and route.handler != handler):
600+
remaining.append(route)
601+
else:
602+
removed.append(route)
603+
await self._unroute_internal(removed, remaining, "default")
604+
605+
async def _unroute_internal(
606+
self,
607+
removed: List[RouteHandler],
608+
remaining: List[RouteHandler],
609+
behavior: Literal["default", "ignoreErrors", "wait"] = None,
610+
) -> None:
611+
self._routes = remaining
612+
await self._update_interception_patterns()
613+
if behavior is None or behavior == "default":
614+
return
615+
await asyncio.gather(
616+
*map(
617+
lambda route: route.stop(behavior), # type: ignore
618+
removed,
594619
)
595620
)
596-
await self._update_interception_patterns()
621+
622+
def _dispose_har_routers(self) -> None:
623+
for router in self._har_routers:
624+
router.dispose()
625+
self._har_routers = []
597626

598627
async def unroute_all(
599628
self, behavior: Literal["default", "ignoreErrors", "wait"] = None
600629
) -> None:
601-
pass
630+
await self._unroute_internal(self._routes, [], behavior)
631+
self._dispose_har_routers()
602632

603633
async def route_from_har(
604634
self,
@@ -624,6 +654,7 @@ async def route_from_har(
624654
not_found_action=notFound or "abort",
625655
url_matcher=url,
626656
)
657+
self._har_routers.append(router)
627658
await router.add_page_route(self)
628659

629660
async def _update_interception_patterns(self) -> None:
@@ -675,6 +706,7 @@ async def title(self) -> str:
675706

676707
async def close(self, runBeforeUnload: bool = None, reason: str = None) -> None:
677708
self._close_reason = reason
709+
self._close_was_called = True
678710
try:
679711
await self._channel.send("close", locals_to_params(locals()))
680712
if self._owned_context:

0 commit comments

Comments
 (0)