Skip to content

Commit efb281e

Browse files
authored
feat: implement routeFromHAR (#960)
1 parent fdec32c commit efb281e

21 files changed

+1875
-22
lines changed

playwright/src/main/java/com/microsoft/playwright/Browser.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,17 @@ class NewContextOptions {
144144
* 'http://per-context' } })}.
145145
*/
146146
public Proxy proxy;
147+
/**
148+
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
149+
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
150+
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
151+
*/
152+
public HarContentPolicy recordHarContent;
153+
/**
154+
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
155+
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
156+
*/
157+
public HarMode recordHarMode;
147158
/**
148159
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
149160
*/
@@ -374,6 +385,23 @@ public NewContextOptions setProxy(Proxy proxy) {
374385
this.proxy = proxy;
375386
return this;
376387
}
388+
/**
389+
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
390+
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
391+
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
392+
*/
393+
public NewContextOptions setRecordHarContent(HarContentPolicy recordHarContent) {
394+
this.recordHarContent = recordHarContent;
395+
return this;
396+
}
397+
/**
398+
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
399+
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
400+
*/
401+
public NewContextOptions setRecordHarMode(HarMode recordHarMode) {
402+
this.recordHarMode = recordHarMode;
403+
return this;
404+
}
377405
/**
378406
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
379407
*/
@@ -602,6 +630,17 @@ class NewPageOptions {
602630
* 'http://per-context' } })}.
603631
*/
604632
public Proxy proxy;
633+
/**
634+
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
635+
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
636+
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
637+
*/
638+
public HarContentPolicy recordHarContent;
639+
/**
640+
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
641+
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
642+
*/
643+
public HarMode recordHarMode;
605644
/**
606645
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
607646
*/
@@ -832,6 +871,23 @@ public NewPageOptions setProxy(Proxy proxy) {
832871
this.proxy = proxy;
833872
return this;
834873
}
874+
/**
875+
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
876+
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
877+
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
878+
*/
879+
public NewPageOptions setRecordHarContent(HarContentPolicy recordHarContent) {
880+
this.recordHarContent = recordHarContent;
881+
return this;
882+
}
883+
/**
884+
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
885+
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
886+
*/
887+
public NewPageOptions setRecordHarMode(HarMode recordHarMode) {
888+
this.recordHarMode = recordHarMode;
889+
return this;
890+
}
835891
/**
836892
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
837893
*/

playwright/src/main/java/com/microsoft/playwright/BrowserType.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,17 @@ class LaunchPersistentContextOptions {
516516
* Network proxy settings.
517517
*/
518518
public Proxy proxy;
519+
/**
520+
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
521+
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
522+
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
523+
*/
524+
public HarContentPolicy recordHarContent;
525+
/**
526+
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
527+
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
528+
*/
529+
public HarMode recordHarMode;
519530
/**
520531
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
521532
*/
@@ -854,6 +865,23 @@ public LaunchPersistentContextOptions setProxy(Proxy proxy) {
854865
this.proxy = proxy;
855866
return this;
856867
}
868+
/**
869+
* Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach}
870+
* is specified, resources are persistet as separate files and all of these files are archived along with the HAR file.
871+
* Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification.
872+
*/
873+
public LaunchPersistentContextOptions setRecordHarContent(HarContentPolicy recordHarContent) {
874+
this.recordHarContent = recordHarContent;
875+
return this;
876+
}
877+
/**
878+
* When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
879+
* security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}.
880+
*/
881+
public LaunchPersistentContextOptions setRecordHarMode(HarMode recordHarMode) {
882+
this.recordHarMode = recordHarMode;
883+
return this;
884+
}
857885
/**
858886
* Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}.
859887
*/

playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
import com.google.gson.JsonElement;
2121
import com.google.gson.JsonObject;
2222
import com.microsoft.playwright.*;
23-
import com.microsoft.playwright.options.BindingCallback;
24-
import com.microsoft.playwright.options.Cookie;
25-
import com.microsoft.playwright.options.FunctionCallback;
26-
import com.microsoft.playwright.options.Geolocation;
23+
import com.microsoft.playwright.options.*;
2724

2825
import java.io.IOException;
2926
import java.net.MalformedURLException;
@@ -336,7 +333,7 @@ public APIRequestContextImpl request() {
336333

337334
@Override
338335
public void route(String url, Consumer<Route> handler, RouteOptions options) {
339-
route(new UrlMatcher(this.baseUrl, url), handler, options);
336+
route(new UrlMatcher(baseUrl, url), handler, options);
340337
}
341338

342339
@Override
@@ -351,7 +348,13 @@ public void route(Predicate<String> url, Consumer<Route> handler, RouteOptions o
351348

352349
@Override
353350
public void routeFromHAR(Path har, RouteFromHAROptions options) {
354-
// TODO:
351+
if (options == null) {
352+
options = new RouteFromHAROptions();
353+
}
354+
UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url);
355+
HARRouter harRouter = new HARRouter(browser.localUtils, har, options.notFound);
356+
onClose(context -> harRouter.dispose());
357+
route(matcher, route -> harRouter.handle(route), null);
355358
}
356359

357360
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {

playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.gson.JsonElement;
2121
import com.google.gson.JsonObject;
2222
import com.microsoft.playwright.*;
23+
import com.microsoft.playwright.options.HarContentPolicy;
2324

2425
import java.io.IOException;
2526
import java.nio.charset.StandardCharsets;
@@ -140,8 +141,13 @@ private BrowserContextImpl newContextImpl(NewContextOptions options) {
140141
if (options.recordHarPath != null) {
141142
recordHar = new JsonObject();
142143
recordHar.addProperty("path", options.recordHarPath.toString());
143-
if (options.recordHarOmitContent != null) {
144-
recordHar.addProperty("omitContent", true);
144+
if (options.recordHarContent != null) {
145+
recordHar.addProperty("content", options.recordHarContent.toString().toLowerCase());
146+
} else if (options.recordHarOmitContent != null && options.recordHarOmitContent) {
147+
recordHar.addProperty("content", HarContentPolicy.OMIT.toString().toLowerCase());
148+
}
149+
if (options.recordHarMode != null) {
150+
recordHar.addProperty("mode", options.recordHarMode.toString().toLowerCase());
145151
}
146152
if (options.recordHarUrlFilter instanceof String) {
147153
recordHar.addProperty("urlGlob", (String) options.recordHarUrlFilter);
@@ -151,7 +157,9 @@ private BrowserContextImpl newContextImpl(NewContextOptions options) {
151157
recordHar.addProperty("urlRegexFlags", toJsRegexFlags(pattern));
152158
}
153159
options.recordHarPath = null;
160+
options.recordHarMode = null;
154161
options.recordHarOmitContent = null;
162+
options.recordHarContent = null;
155163
options.recordHarUrlFilter = null;
156164
} else {
157165
if (options.recordHarOmitContent != null) {
@@ -160,6 +168,12 @@ private BrowserContextImpl newContextImpl(NewContextOptions options) {
160168
if (options.recordHarUrlFilter != null) {
161169
throw new PlaywrightException("recordHarUrlFilter is set but recordHarPath is null");
162170
}
171+
if (options.recordHarMode != null) {
172+
throw new PlaywrightException("recordHarMode is set but recordHarPath is null");
173+
}
174+
if (options.recordHarContent != null) {
175+
throw new PlaywrightException("recordHarContent is set but recordHarPath is null");
176+
}
163177
}
164178

165179
JsonObject params = gson().toJsonTree(options).getAsJsonObject();
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.microsoft.playwright.impl;
18+
19+
import com.google.gson.JsonObject;
20+
import com.microsoft.playwright.PlaywrightException;
21+
import com.microsoft.playwright.Request;
22+
import com.microsoft.playwright.Route;
23+
import com.microsoft.playwright.options.HarNotFound;
24+
25+
import java.nio.file.Path;
26+
import java.util.Base64;
27+
import java.util.Map;
28+
29+
import static com.microsoft.playwright.impl.LoggingSupport.logApi;
30+
import static com.microsoft.playwright.impl.Serialization.fromNameValues;
31+
import static com.microsoft.playwright.impl.Serialization.gson;
32+
33+
public class HARRouter {
34+
private final LocalUtils localUtils;
35+
private final HarNotFound defaultAction;
36+
private final String harId;
37+
38+
HARRouter(LocalUtils localUtils, Path harFile, HarNotFound defaultAction) {
39+
this.localUtils = localUtils;
40+
this.defaultAction = defaultAction;
41+
42+
JsonObject params = new JsonObject();
43+
params.addProperty("file", harFile.toString());
44+
JsonObject json = localUtils.sendMessage("harOpen", params).getAsJsonObject();
45+
if (json.has("error")) {
46+
throw new PlaywrightException(json.get("error").getAsString());
47+
}
48+
harId = json.get("harId").getAsString();
49+
}
50+
51+
void handle(Route route) {
52+
Request request = route.request();
53+
54+
JsonObject params = new JsonObject();
55+
params.addProperty("harId", harId);
56+
params.addProperty("url", request.url());
57+
params.addProperty("method", request.method());
58+
params.add("headers", gson().toJsonTree(request.headersArray()));
59+
if (request.postDataBuffer() != null) {
60+
String base64 = Base64.getEncoder().encodeToString(request.postDataBuffer());
61+
params.addProperty("postData", base64);
62+
}
63+
params.addProperty("isNavigationRequest", request.isNavigationRequest());
64+
JsonObject response = localUtils.sendMessage("harLookup", params).getAsJsonObject();
65+
66+
String action = response.get("action").getAsString();
67+
if ("redirect".equals(action)) {
68+
String redirectURL = response.get("redirectURL").getAsString();
69+
logApi("HAR: " + route.request().url() + " redirected to " + redirectURL);
70+
((RouteImpl) route).redirectNavigationRequest(redirectURL);
71+
return;
72+
}
73+
74+
if ("fulfill".equals(action)) {
75+
int status = response.get("status").getAsInt();
76+
Map<String, String> headers = fromNameValues(response.getAsJsonArray("headers"));
77+
byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString());
78+
route.fulfill(new Route.FulfillOptions()
79+
.setStatus(status)
80+
.setHeaders(headers)
81+
.setBodyBytes(buffer));
82+
return;
83+
}
84+
85+
if ("error".equals(action)) {
86+
logApi("HAR: " + response.get("message").getAsString());
87+
// Report the error, but fall through to the default handler.
88+
}
89+
90+
if (defaultAction == HarNotFound.FALLBACK) {
91+
route.fallback();
92+
return;
93+
}
94+
95+
// By default abort not matching requests.
96+
route.abort();
97+
}
98+
99+
void dispose() {
100+
JsonObject params = new JsonObject();
101+
params.addProperty("harId", harId);
102+
localUtils.sendMessageAsync("harClose", params);
103+
}
104+
}

playwright/src/main/java/com/microsoft/playwright/impl/LoggingSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ static void logWithTimestamp(String message) {
6060
System.err.println(timestamp + " " + message);
6161
}
6262

63-
private void logApi(String message) {
63+
static void logApi(String message) {
6464
// This matches log format produced by the server.
6565
logWithTimestamp("pw:api " + message);
6666
}

playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -971,7 +971,13 @@ public void route(Predicate<String> url, Consumer<Route> handler, RouteOptions o
971971

972972
@Override
973973
public void routeFromHAR(Path har, RouteFromHAROptions options) {
974-
// TODO:
974+
if (options == null) {
975+
options = new RouteFromHAROptions();
976+
}
977+
UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url);
978+
HARRouter harRouter = new HARRouter(browserContext.browser().localUtils, har, options.notFound);
979+
onClose(context -> harRouter.dispose());
980+
route(matcher, route -> harRouter.handle(route), null);
975981
}
976982

977983
private void route(UrlMatcher matcher, Consumer<Route> handler, RouteOptions options) {

playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ public RequestImpl request() {
199199
return connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
200200
}
201201

202+
void redirectNavigationRequest(String redirectURL) {
203+
startHandling();
204+
JsonObject params = new JsonObject();
205+
params.addProperty("url", redirectURL);
206+
// TODO: _raceWithPageClose ?
207+
sendMessageAsync("redirectNavigationRequest", params);
208+
}
209+
202210
private void startHandling() {
203211
if (handled) {
204212
throw new PlaywrightException("Route is already handled!");

playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import java.nio.charset.StandardCharsets;
3232
import java.nio.file.Path;
3333
import java.util.*;
34-
import java.util.regex.Pattern;
3534

3635
class Serialization {
3736
private static final Gson gson = new GsonBuilder()
@@ -311,6 +310,15 @@ static JsonArray toNameValueArray(Map<String, ?> map) {
311310
return array;
312311
}
313312

313+
static Map<String, String> fromNameValues(JsonArray array) {
314+
Map<String, String> map = new LinkedHashMap<>();
315+
for (JsonElement element : array) {
316+
JsonObject pair = element.getAsJsonObject();
317+
map.put(pair.get("name").getAsString(), pair.get("value").getAsString());
318+
}
319+
return map;
320+
}
321+
314322
static List<String> parseStringList(JsonArray array) {
315323
List<String> result = new ArrayList<>();
316324
for (JsonElement e : array) {

0 commit comments

Comments
 (0)