Skip to content

Commit b2e3b93

Browse files
authoredJun 28, 2022
chore: port routeFromHar, roll to 1.23.0 driver (#1384)
This is part 5/n of the 1.23 port. Relates #1308, #1374, #1376, #1382, #1383 Ports: - [x] microsoft/playwright@259c8d6 (feat: Page.routeFromHar (#14870)) - [x] microsoft/playwright@79378dd (fix: add pw:api logging to har router (#14903)) - [x] microsoft/playwright@030e7d2 (chore(har): allow replaying from zip har (#14962)) - [x] microsoft/playwright@ed6b14f (fix(har): restart redirected navigation (#14939)) - [x] microsoft/playwright@e5372c3 (chore: move har router into local utils (#14967)) - [x] microsoft/playwright@920f1d5 (chore: allow routing by uncompressed har (#14987)) - [x] microsoft/playwright@eb87966 (feat(har): disambiguate requests by post data (#14993)) - [x] microsoft/playwright@6af6fab (fix(har): internal redirect in renderer-initiated navigations (#15000)) - [x] microsoft/playwright@9525bed (feat(har): re-add routeFromHAR (#15024))
1 parent 7b424eb commit b2e3b93

22 files changed

+2120
-38
lines changed
 

‎.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ repos:
66
hooks:
77
- id: trailing-whitespace
88
- id: end-of-file-fixer
9+
exclude: tests/assets/har-sha1-main-response.txt
910
- id: check-yaml
1011
- id: check-toml
1112
- id: requirements-txt-fixer

‎README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Playwright is a Python library to automate [Chromium](https://www.chromium.org/H
44

55
| | Linux | macOS | Windows |
66
| :--- | :---: | :---: | :---: |
7-
| Chromium <!-- GEN:chromium-version -->103.0.5060.53<!-- GEN:stop --> ||||
7+
| Chromium <!-- GEN:chromium-version -->104.0.5112.20<!-- GEN:stop --> ||||
88
| WebKit <!-- GEN:webkit-version -->15.4<!-- GEN:stop --> ||||
99
| Firefox <!-- GEN:firefox-version -->100.0.2<!-- GEN:stop --> ||||
1010

‎playwright/_impl/_browser_context.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
from playwright._impl._event_context_manager import EventContextManagerImpl
3636
from playwright._impl._fetch import APIRequestContext
3737
from playwright._impl._frame import Frame
38+
from playwright._impl._har_router import HarRouter
3839
from playwright._impl._helper import (
40+
RouteFromHarNotFoundPolicy,
3941
RouteHandler,
4042
RouteHandlerCallback,
4143
TimeoutSettings,
@@ -292,6 +294,20 @@ async def unroute(
292294
if len(self._routes) == 0:
293295
await self._disable_interception()
294296

297+
async def route_from_har(
298+
self,
299+
har: Union[Path, str],
300+
url: URLMatch = None,
301+
not_found: RouteFromHarNotFoundPolicy = None,
302+
) -> None:
303+
router = await HarRouter.create(
304+
local_utils=self._connection.local_utils,
305+
file=str(har),
306+
not_found_action=not_found or "abort",
307+
url_matcher=url,
308+
)
309+
await router.add_context_route(self)
310+
295311
async def _disable_interception(self) -> None:
296312
await self._channel.send("setNetworkInterceptionEnabled", dict(enabled=False))
297313

‎playwright/_impl/_har_router.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import asyncio
2+
import base64
3+
from typing import TYPE_CHECKING, Optional, cast
4+
5+
from playwright._impl._api_structures import HeadersArray
6+
from playwright._impl._helper import (
7+
HarLookupResult,
8+
RouteFromHarNotFoundPolicy,
9+
URLMatch,
10+
)
11+
from playwright._impl._local_utils import LocalUtils
12+
13+
if TYPE_CHECKING: # pragma: no cover
14+
from playwright._impl._browser_context import BrowserContext
15+
from playwright._impl._network import Route
16+
from playwright._impl._page import Page
17+
18+
19+
class HarRouter:
20+
def __init__(
21+
self,
22+
local_utils: LocalUtils,
23+
har_id: str,
24+
not_found_action: RouteFromHarNotFoundPolicy,
25+
url_matcher: Optional[URLMatch] = None,
26+
) -> None:
27+
self._local_utils: LocalUtils = local_utils
28+
self._har_id: str = har_id
29+
self._not_found_action: RouteFromHarNotFoundPolicy = not_found_action
30+
self._options_url_match: Optional[URLMatch] = url_matcher
31+
32+
@staticmethod
33+
async def create(
34+
local_utils: LocalUtils,
35+
file: str,
36+
not_found_action: RouteFromHarNotFoundPolicy,
37+
url_matcher: Optional[URLMatch] = None,
38+
) -> "HarRouter":
39+
har_id = await local_utils._channel.send("harOpen", {"file": file})
40+
return HarRouter(
41+
local_utils=local_utils,
42+
har_id=har_id,
43+
not_found_action=not_found_action,
44+
url_matcher=url_matcher,
45+
)
46+
47+
async def _handle(self, route: "Route") -> None:
48+
request = route.request
49+
response: HarLookupResult = await self._local_utils.har_lookup(
50+
harId=self._har_id,
51+
url=request.url,
52+
method=request.method,
53+
headers=await request.headers_array(),
54+
postData=request.post_data_buffer,
55+
isNavigationRequest=request.is_navigation_request(),
56+
)
57+
action = response["action"]
58+
if action == "redirect":
59+
redirect_url = response["redirectURL"]
60+
assert redirect_url
61+
await route._redirected_navigation_request(redirect_url)
62+
return
63+
64+
if action == "fulfill":
65+
body = response["body"]
66+
assert body is not None
67+
await route.fulfill(
68+
status=response.get("status"),
69+
headers={
70+
v["name"]: v["value"]
71+
for v in cast(HeadersArray, response.get("headers", []))
72+
},
73+
body=base64.b64decode(body),
74+
)
75+
return
76+
77+
if action == "error":
78+
pass
79+
# Report the error, but fall through to the default handler.
80+
81+
if self._not_found_action == "abort":
82+
await route.abort()
83+
return
84+
85+
await route.fallback()
86+
87+
async def add_context_route(self, context: "BrowserContext") -> None:
88+
await context.route(
89+
url=self._options_url_match or "**/*",
90+
handler=lambda route, _: asyncio.create_task(self._handle(route)),
91+
)
92+
context.once("close", lambda _: self._dispose())
93+
94+
async def add_page_route(self, page: "Page") -> None:
95+
await page.route(
96+
url=self._options_url_match or "**/*",
97+
handler=lambda route, _: asyncio.create_task(self._handle(route)),
98+
)
99+
page.once("close", lambda _: self._dispose())
100+
101+
def _dispose(self) -> None:
102+
asyncio.create_task(
103+
self._local_utils._channel.send("harClose", {"harId": self._har_id})
104+
)

‎playwright/_impl/_helper.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949

5050

5151
if TYPE_CHECKING: # pragma: no cover
52+
from playwright._impl._api_structures import HeadersArray
5253
from playwright._impl._network import Request, Response, Route
5354

5455
URLMatch = Union[str, Pattern, Callable[[str], bool]]
@@ -67,6 +68,7 @@
6768
ServiceWorkersPolicy = Literal["allow", "block"]
6869
HarMode = Literal["full", "minimal"]
6970
HarContentPolicy = Literal["attach", "embed", "omit"]
71+
RouteFromHarNotFoundPolicy = Literal["abort", "fallback"]
7072

7173

7274
class ErrorPayload(TypedDict, total=False):
@@ -135,6 +137,15 @@ def matches(self, url: str) -> bool:
135137
return False
136138

137139

140+
class HarLookupResult(TypedDict, total=False):
141+
action: Literal["error", "redirect", "fulfill", "noentry"]
142+
message: Optional[str]
143+
redirectURL: Optional[str]
144+
status: Optional[int]
145+
headers: Optional["HeadersArray"]
146+
body: Optional[str]
147+
148+
138149
class TimeoutSettings:
139150
def __init__(self, parent: Optional["TimeoutSettings"]) -> None:
140151
self._parent = parent

‎playwright/_impl/_local_utils.py

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

15-
from typing import Dict, List
15+
import base64
16+
from typing import Dict, List, Optional, cast
1617

17-
from playwright._impl._api_structures import NameValue
18+
from playwright._impl._api_structures import HeadersArray, NameValue
1819
from playwright._impl._connection import ChannelOwner
20+
from playwright._impl._helper import HarLookupResult, locals_to_params
1921

2022

2123
class LocalUtils(ChannelOwner):
@@ -26,3 +28,28 @@ def __init__(
2628

2729
async def zip(self, zip_file: str, entries: List[NameValue]) -> None:
2830
await self._channel.send("zip", {"zipFile": zip_file, "entries": entries})
31+
32+
async def har_open(self, file: str) -> None:
33+
params = locals_to_params(locals())
34+
await self._channel.send("harOpen", params)
35+
36+
async def har_lookup(
37+
self,
38+
harId: str,
39+
url: str,
40+
method: str,
41+
headers: HeadersArray,
42+
isNavigationRequest: bool,
43+
postData: Optional[bytes] = None,
44+
) -> HarLookupResult:
45+
params = locals_to_params(locals())
46+
if "postData" in params:
47+
params["postData"] = base64.b64encode(params["postData"]).decode()
48+
return cast(
49+
HarLookupResult,
50+
await self._channel.send_return_as_dict("harLookup", params),
51+
)
52+
53+
async def har_close(self, harId: str) -> None:
54+
params = locals_to_params(locals())
55+
await self._channel.send("harClose", params)

‎playwright/_impl/_network.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,6 @@ async def continue_route() -> None:
355355

356356
return continue_route()
357357

358-
# FIXME: Port corresponding tests, and call this method
359358
async def _redirected_navigation_request(self, url: str) -> None:
360359
self._check_not_handled()
361360
await self._race_with_page_close(

‎playwright/_impl/_page.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@
5353
from playwright._impl._event_context_manager import EventContextManagerImpl
5454
from playwright._impl._file_chooser import FileChooser
5555
from playwright._impl._frame import Frame
56+
from playwright._impl._har_router import HarRouter
5657
from playwright._impl._helper import (
5758
ColorScheme,
5859
DocumentLoadState,
5960
ForcedColors,
6061
KeyboardModifier,
6162
MouseButton,
6263
ReducedMotion,
64+
RouteFromHarNotFoundPolicy,
6365
RouteHandler,
6466
RouteHandlerCallback,
6567
TimeoutSettings,
@@ -600,6 +602,20 @@ async def unroute(
600602
if len(self._routes) == 0:
601603
await self._disable_interception()
602604

605+
async def route_from_har(
606+
self,
607+
har: Union[Path, str],
608+
url: URLMatch = None,
609+
not_found: RouteFromHarNotFoundPolicy = None,
610+
) -> None:
611+
router = await HarRouter.create(
612+
local_utils=self._connection.local_utils,
613+
file=str(har),
614+
not_found_action=not_found or "abort",
615+
url_matcher=url,
616+
)
617+
await router.add_page_route(self)
618+
603619
async def _disable_interception(self) -> None:
604620
await self._channel.send("setNetworkInterceptionEnabled", dict(enabled=False))
605621

‎playwright/async_api/_generated.py

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ def timing(self) -> ResourceTiming:
268268
def headers(self) -> typing.Dict[str, str]:
269269
"""Request.headers
270270

271-
**DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `request.all_headers()` instead.
271+
An object with the request HTTP headers. The header names are lower-cased. Note that this method does not return
272+
security-related headers, including cookie-related ones. You can use `request.all_headers()` for complete list of
273+
headers that include `cookie` information.
272274

273275
Returns
274276
-------
@@ -411,7 +413,9 @@ def status_text(self) -> str:
411413
def headers(self) -> typing.Dict[str, str]:
412414
"""Response.headers
413415

414-
**DEPRECATED** Incomplete list of headers as seen by the rendering engine. Use `response.all_headers()` instead.
416+
An object with the response HTTP headers. The header names are lower-cased. Note that this method does not return
417+
security-related headers, including cookie-related ones. You can use `response.all_headers()` for complete list
418+
of headers that include `cookie` information.
415419

416420
Returns
417421
-------
@@ -7736,6 +7740,43 @@ async def unroute(
77367740
)
77377741
)
77387742

7743+
async def route_from_har(
7744+
self,
7745+
har: typing.Union[pathlib.Path, str],
7746+
*,
7747+
url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None,
7748+
not_found: Literal["abort", "fallback"] = None
7749+
) -> NoneType:
7750+
"""Page.route_from_har
7751+
7752+
If specified the network requests that are made in the page will be served from the HAR file. Read more about
7753+
[Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har).
7754+
7755+
Playwright will not serve requests intercepted by Service Worker from the HAR file. See
7756+
[this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using
7757+
request interception by setting `Browser.newContext.serviceWorkers` to `'block'`.
7758+
7759+
Parameters
7760+
----------
7761+
har : Union[pathlib.Path, str]
7762+
Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a
7763+
relative path, then it is resolved relative to the current working directory.
7764+
url : Union[Callable[[str], bool], Pattern, str, NoneType]
7765+
A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
7766+
will be surved from the HAR file. If not specified, all requests are served from the HAR file.
7767+
not_found : Union["abort", "fallback", NoneType]
7768+
- If set to 'abort' any request not found in the HAR file will be aborted.
7769+
- If set to 'fallback' missing requests will be sent to the network.
7770+
7771+
Defaults to abort.
7772+
"""
7773+
7774+
return mapping.from_maybe_impl(
7775+
await self._impl_obj.route_from_har(
7776+
har=har, url=self._wrap_handler(url), not_found=not_found
7777+
)
7778+
)
7779+
77397780
async def screenshot(
77407781
self,
77417782
*,
@@ -10432,6 +10473,43 @@ async def unroute(
1043210473
)
1043310474
)
1043410475

10476+
async def route_from_har(
10477+
self,
10478+
har: typing.Union[pathlib.Path, str],
10479+
*,
10480+
url: typing.Union[str, typing.Pattern, typing.Callable[[str], bool]] = None,
10481+
not_found: Literal["abort", "fallback"] = None
10482+
) -> NoneType:
10483+
"""BrowserContext.route_from_har
10484+
10485+
If specified the network requests that are made in the context will be served from the HAR file. Read more about
10486+
[Replaying from HAR](https://playwright.dev/python/docs/network#replaying-from-har).
10487+
10488+
Playwright will not serve requests intercepted by Service Worker from the HAR file. See
10489+
[this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using
10490+
request interception by setting `Browser.newContext.serviceWorkers` to `'block'`.
10491+
10492+
Parameters
10493+
----------
10494+
har : Union[pathlib.Path, str]
10495+
Path to a [HAR](http://www.softwareishard.com/blog/har-12-spec) file with prerecorded network data. If `path` is a
10496+
relative path, then it is resolved relative to the current working directory.
10497+
url : Union[Callable[[str], bool], Pattern, str, NoneType]
10498+
A glob pattern, regular expression or predicate to match the request URL. Only requests with URL matching the pattern
10499+
will be surved from the HAR file. If not specified, all requests are served from the HAR file.
10500+
not_found : Union["abort", "fallback", NoneType]
10501+
- If set to 'abort' any request not found in the HAR file will be aborted.
10502+
- If set to 'fallback' falls through to the next route handler in the handler chain.
10503+
10504+
Defaults to abort.
10505+
"""
10506+
10507+
return mapping.from_maybe_impl(
10508+
await self._impl_obj.route_from_har(
10509+
har=har, url=self._wrap_handler(url), not_found=not_found
10510+
)
10511+
)
10512+
1043510513
def expect_event(
1043610514
self, event: str, predicate: typing.Callable = None, *, timeout: float = None
1043710515
) -> AsyncEventContextManager:

0 commit comments

Comments
 (0)