Skip to content

Commit b205399

Browse files
authored
Cooperative request intercepts (#2403)
* some progress * cr * cr * requestId is string * unflake test * prettier
1 parent 408e3ea commit b205399

23 files changed

+1587
-144
lines changed

lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs

Lines changed: 787 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Net.Http;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using PuppeteerSharp.Tests.Attributes;
7+
using PuppeteerSharp.Nunit;
8+
using NUnit.Framework;
9+
10+
namespace PuppeteerSharp.Tests.RequestInterceptionExperimentalTests;
11+
12+
public class RequestContinueTests : PuppeteerPageBaseTest
13+
{
14+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should work")]
15+
[Skip(SkipAttribute.Targets.Firefox)]
16+
public async Task ShouldWork()
17+
{
18+
await Page.SetRequestInterceptionAsync(true);
19+
Page.AddRequestInterceptor(async request => await request.ContinueAsync(new Payload(), 0));
20+
await Page.GoToAsync(TestConstants.EmptyPage);
21+
}
22+
23+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should amend HTTP headers")]
24+
[Skip(SkipAttribute.Targets.Firefox)]
25+
public async Task ShouldAmendHTTPHeaders()
26+
{
27+
await Page.SetRequestInterceptionAsync(true);
28+
Page.AddRequestInterceptor(request =>
29+
{
30+
var headers = new Dictionary<string, string>(request.Headers) { ["FOO"] = "bar" };
31+
return request.ContinueAsync(new Payload { Headers = headers }, 0);
32+
});
33+
await Page.GoToAsync(TestConstants.EmptyPage);
34+
var requestTask = Server.WaitForRequest("/sleep.zzz", request => request.Headers["foo"].ToString());
35+
await Task.WhenAll(
36+
requestTask,
37+
Page.EvaluateExpressionAsync("fetch('/sleep.zzz')")
38+
);
39+
Assert.AreEqual("bar", requestTask.Result);
40+
}
41+
42+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue",
43+
"should redirect in a way non-observable to page")]
44+
[Skip(SkipAttribute.Targets.Firefox)]
45+
public async Task ShouldRedirectInAWayNonObservableToPage()
46+
{
47+
await Page.SetRequestInterceptionAsync(true);
48+
Page.AddRequestInterceptor(request =>
49+
{
50+
var redirectURL = request.Url.Contains("/empty.html")
51+
? TestConstants.ServerUrl + "/consolelog.html"
52+
: null;
53+
return request.ContinueAsync(new Payload { Url = redirectURL }, 0);
54+
});
55+
string consoleMessage = null;
56+
Page.Console += (_, e) => consoleMessage = e.Message.Text;
57+
await Page.GoToAsync(TestConstants.EmptyPage);
58+
Assert.AreEqual(TestConstants.EmptyPage, Page.Url);
59+
Assert.AreEqual("yellow", consoleMessage);
60+
}
61+
62+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should amend method")]
63+
[Skip(SkipAttribute.Targets.Firefox)]
64+
public async Task ShouldAmendMethodData()
65+
{
66+
await Page.GoToAsync(TestConstants.EmptyPage);
67+
await Page.SetRequestInterceptionAsync(true);
68+
Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload { Method = HttpMethod.Post }, 0));
69+
70+
var requestTask = Server.WaitForRequest<string>("/sleep.zzz", request => request.Method);
71+
72+
await Task.WhenAll(
73+
requestTask,
74+
Page.EvaluateExpressionAsync("fetch('/sleep.zzz')")
75+
);
76+
77+
Assert.AreEqual("POST", requestTask.Result);
78+
}
79+
80+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should amend post data")]
81+
[Skip(SkipAttribute.Targets.Firefox)]
82+
public async Task ShouldAmendPostData()
83+
{
84+
await Page.SetRequestInterceptionAsync(true);
85+
Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload { Method = HttpMethod.Post, PostData = "doggo" }, 0));
86+
var requestTask = Server.WaitForRequest("/sleep.zzz", async request =>
87+
{
88+
using var reader = new StreamReader(request.Body, Encoding.UTF8);
89+
return await reader.ReadToEndAsync();
90+
});
91+
92+
await Task.WhenAll(
93+
requestTask,
94+
Page.GoToAsync(TestConstants.ServerUrl + "/sleep.zzz")
95+
);
96+
97+
Assert.AreEqual("doggo", await requestTask.Result);
98+
}
99+
100+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue",
101+
"should amend both post data and method on navigation")]
102+
[Skip(SkipAttribute.Targets.Firefox)]
103+
public async Task ShouldAmendBothPostDataAndMethodOnNavigation()
104+
{
105+
await Page.SetRequestInterceptionAsync(true);
106+
Page.AddRequestInterceptor(request => request.ContinueAsync(
107+
new Payload
108+
{
109+
Method = HttpMethod.Post, PostData = "doggo"
110+
},
111+
0));
112+
113+
var serverRequestTask = Server.WaitForRequest("/empty.html", async req =>
114+
{
115+
var body = await new StreamReader(req.Body).ReadToEndAsync();
116+
return new { req.Method, Body = body };
117+
});
118+
119+
await Task.WhenAll(
120+
serverRequestTask,
121+
Page.GoToAsync(TestConstants.EmptyPage)
122+
);
123+
var serverRequest = await serverRequestTask;
124+
Assert.AreEqual(HttpMethod.Post.Method, serverRequest.Result.Method);
125+
Assert.AreEqual("doggo", serverRequest.Result.Body);
126+
}
127+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Net;
4+
using System.Threading.Tasks;
5+
using PuppeteerSharp.Tests.Attributes;
6+
using PuppeteerSharp.Nunit;
7+
using NUnit.Framework;
8+
9+
namespace PuppeteerSharp.Tests.RequestInterceptionExperimentalTests;
10+
11+
public class RequestRespondTests : PuppeteerPageBaseTest
12+
{
13+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should work")]
14+
[Skip(SkipAttribute.Targets.Firefox)]
15+
public async Task ShouldWork()
16+
{
17+
await Page.SetRequestInterceptionAsync(true);
18+
Page.AddRequestInterceptor(request => request.RespondAsync(new ResponseData
19+
{
20+
Status = HttpStatusCode.Created,
21+
Headers = new Dictionary<string, object> { ["foo"] = "bar" },
22+
Body = "Yo, page!"
23+
}, 0));
24+
25+
var response = await Page.GoToAsync(TestConstants.EmptyPage);
26+
Assert.AreEqual(HttpStatusCode.Created, response.Status);
27+
Assert.AreEqual("bar", response.Headers["foo"]);
28+
Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync<string>("document.body.textContent"));
29+
}
30+
31+
/// <summary>
32+
/// In puppeteer this method is called ShouldWorkWithStatusCode422.
33+
/// I found that status 422 is not available in all .NET runtimes (see https://github.com/dotnet/core/blob/4c4642d548074b3fbfd425541a968aadd75fea99/release-notes/2.1/Preview/api-diff/preview2/2.1-preview2_System.Net.md)
34+
/// As the goal here is testing HTTP codes that are not in Chromium (see https://cs.chromium.org/chromium/src/net/http/http_status_code_list.h?sq=package:chromium&g=0) we will use code 426: Upgrade Required
35+
/// </summary>
36+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should work with status code 422")]
37+
[Skip(SkipAttribute.Targets.Firefox)]
38+
public async Task ShouldWorkReturnStatusPhrases()
39+
{
40+
await Page.SetRequestInterceptionAsync(true);
41+
Page.AddRequestInterceptor(request => request.RespondAsync(new ResponseData
42+
{
43+
Status = HttpStatusCode.UpgradeRequired, Body = "Yo, page!"
44+
}, 0));
45+
46+
var response = await Page.GoToAsync(TestConstants.EmptyPage);
47+
Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.Status);
48+
Assert.AreEqual("Upgrade Required", response.StatusText);
49+
Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync<string>("document.body.textContent"));
50+
}
51+
52+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should redirect")]
53+
[Skip(SkipAttribute.Targets.Firefox)]
54+
public async Task ShouldRedirect()
55+
{
56+
await Page.SetRequestInterceptionAsync(true);
57+
58+
Page.AddRequestInterceptor(request =>
59+
{
60+
if (!request.Url.Contains("rrredirect"))
61+
{
62+
return request.ContinueAsync(new Payload(), 0);
63+
}
64+
65+
return request.RespondAsync(new ResponseData
66+
{
67+
Status = HttpStatusCode.Redirect,
68+
Headers = new Dictionary<string, object> { ["location"] = TestConstants.EmptyPage }
69+
}, 0);
70+
});
71+
72+
var response = await Page.GoToAsync(TestConstants.ServerUrl + "/rrredirect");
73+
74+
Assert.That(response.Request.RedirectChain, Has.Exactly(1).Items);
75+
Assert.AreEqual(TestConstants.ServerUrl + "/rrredirect", response.Request.RedirectChain[0].Url);
76+
Assert.AreEqual(TestConstants.EmptyPage, response.Url);
77+
}
78+
79+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should allow mocking binary responses")]
80+
[Skip(SkipAttribute.Targets.Firefox)]
81+
public async Task ShouldAllowMockingBinaryResponses()
82+
{
83+
await Page.SetRequestInterceptionAsync(true);
84+
Page.AddRequestInterceptor(request =>
85+
{
86+
var imageData = File.ReadAllBytes("./Assets/pptr.png");
87+
return request.RespondAsync(new ResponseData { ContentType = "image/png", BodyData = imageData }, 0);
88+
});
89+
90+
await Page.EvaluateFunctionAsync(@"PREFIX =>
91+
{
92+
const img = document.createElement('img');
93+
img.src = PREFIX + '/does-not-exist.png';
94+
document.body.appendChild(img);
95+
return new Promise(fulfill => img.onload = fulfill);
96+
}", TestConstants.ServerUrl);
97+
var img = await Page.QuerySelectorAsync("img");
98+
Assert.True(ScreenshotHelper.PixelMatch("mock-binary-response.png", await img.ScreenshotDataAsync()));
99+
}
100+
101+
[PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond",
102+
"should stringify intercepted request response headers")]
103+
[Skip(SkipAttribute.Targets.Firefox)]
104+
public async Task ShouldStringifyInterceptedRequestResponseHeaders()
105+
{
106+
await Page.SetRequestInterceptionAsync(true);
107+
Page.AddRequestInterceptor(request => request.RespondAsync(new ResponseData
108+
{
109+
Status = HttpStatusCode.OK,
110+
Headers = new Dictionary<string, object> { ["foo"] = true },
111+
Body = "Yo, page!"
112+
}, 0));
113+
114+
var response = await Page.GoToAsync(TestConstants.EmptyPage);
115+
Assert.AreEqual(HttpStatusCode.OK, response.Status);
116+
Assert.AreEqual("True", response.Headers["foo"]);
117+
Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync<string>("document.body.textContent"));
118+
}
119+
120+
[Skip(SkipAttribute.Targets.Firefox)]
121+
public async Task ShouldAllowMultipleInterceptedRequestResponseHeaders()
122+
{
123+
await Page.SetRequestInterceptionAsync(true);
124+
Page.AddRequestInterceptor(request =>
125+
{
126+
return request.RespondAsync(new ResponseData
127+
{
128+
Status = HttpStatusCode.OK,
129+
Headers = new Dictionary<string, object>
130+
{
131+
["foo"] = new [] { true, false },
132+
["Set-Cookie"] = new [] { "sessionId=abcdef", "specialId=123456" }
133+
},
134+
Body = "Yo, page!"
135+
}, 0);
136+
});
137+
138+
var response = await Page.GoToAsync(TestConstants.EmptyPage);
139+
var cookies = await Page.GetCookiesAsync(TestConstants.EmptyPage);
140+
141+
Assert.AreEqual(HttpStatusCode.OK, response.Status);
142+
Assert.AreEqual("True\nFalse", response.Headers["foo"]);
143+
Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync<string>("document.body.textContent"));
144+
Assert.AreEqual("specialId", cookies[0].Name);
145+
Assert.AreEqual("123456", cookies[0].Value);
146+
Assert.AreEqual("sessionId", cookies[1].Name);
147+
Assert.AreEqual("abcdef", cookies[1].Value);
148+
}
149+
}

lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,6 @@ namespace PuppeteerSharp.Tests.RequestInterceptionTests
1515
{
1616
public class SetRequestInterceptionTests : PuppeteerPageBaseTest
1717
{
18-
public SetRequestInterceptionTests(): base()
19-
{
20-
}
21-
2218
[PuppeteerTest("requestinterception.spec.ts", "Page.setRequestInterception", "should intercept")]
2319
[Skip(SkipAttribute.Targets.Firefox)]
2420
public async Task ShouldIntercept()
@@ -51,7 +47,7 @@ public async Task ShouldIntercept()
5147

5248
[PuppeteerTest("requestinterception.spec.ts", "Page.setRequestInterception", "should work when POST is redirected with 302")]
5349
[Skip(SkipAttribute.Targets.Firefox)]
54-
public async Task ShouldWorkWhenPostIsEedirectedWith302()
50+
public async Task ShouldWorkWhenPostIsRedirectedWith302()
5551
{
5652
Server.SetRedirect("/rredirect", "/empty.html");
5753
await Page.GoToAsync(TestConstants.EmptyPage);
-27.2 KB
Loading

lib/PuppeteerSharp/BrowserData/Firefox.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static class Firefox
1515
/// </summary>
1616
public const string DefaultBuildId = "FIREFOX_NIGHTLY";
1717

18-
private static readonly Dictionary<string, string> _cachedBuildIds = new();
18+
private static readonly Dictionary<string, string> _cachedBuildIds = [];
1919

2020
internal static Task<string> GetDefaultBuildIdAsync() => ResolveBuildIdAsync(DefaultBuildId);
2121

lib/PuppeteerSharp/IPage.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,8 +1098,8 @@ public interface IPage : IDisposable, IAsyncDisposable
10981098
Task SetOfflineModeAsync(bool value);
10991099

11001100
/// <summary>
1101-
/// Activating request interception enables <see cref="PuppeteerSharp.Request.AbortAsync(RequestAbortErrorCode)">request.AbortAsync</see>,
1102-
/// <see cref="PuppeteerSharp.Request.ContinueAsync(Payload)">request.ContinueAsync</see> and <see cref="PuppeteerSharp.Request.RespondAsync(ResponseData)">request.RespondAsync</see> methods.
1101+
/// Activating request interception enables <see cref="PuppeteerSharp.Request.AbortAsync(RequestAbortErrorCode, int?)">request.AbortAsync</see>,
1102+
/// <see cref="PuppeteerSharp.Request.ContinueAsync(Payload, int?)">request.ContinueAsync</see> and <see cref="PuppeteerSharp.Request.RespondAsync(ResponseData, int?)">request.RespondAsync</see> methods.
11031103
/// </summary>
11041104
/// <returns>The request interception task.</returns>
11051105
/// <param name="value">Whether to enable request interception..</param>
@@ -1438,5 +1438,19 @@ public interface IPage : IDisposable, IAsyncDisposable
14381438
/// <param name="options">Optional waiting parameters.</param>
14391439
/// <returns>A task that resolves after the page gets the prompt.</returns>
14401440
Task<DeviceRequestPrompt> WaitForDevicePromptAsync(WaitTimeoutOptions options = null);
1441+
1442+
/// <summary>
1443+
/// <see cref="IRequest.RespondAsync"/>, <see cref="IRequest.AbortAsync"/>, and <see cref="IRequest.ContinueAsync"/> can accept an optional `priority` to activate Cooperative Intercept Mode.
1444+
/// In Cooperative Mode, all interception tasks are guaranteed to run and all async handlers are awaited.
1445+
/// The interception is resolved to the highest-priority resolution.
1446+
/// </summary>
1447+
/// <param name="interceptionTask">Interception task.</param>
1448+
void AddRequestInterceptor(Func<IRequest, Task> interceptionTask);
1449+
1450+
/// <summary>
1451+
/// Removes a previously added request interceptor.
1452+
/// </summary>
1453+
/// <param name="interceptionTask">Interception task.</param>
1454+
void RemoveRequestInterceptor(Func<IRequest, Task> interceptionTask);
14411455
}
14421456
}

lib/PuppeteerSharp/IRequest.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ public interface IRequest
105105
/// <value>The redirect chain.</value>
106106
IRequest[] RedirectChain { get; }
107107

108+
/// <summary>
109+
/// Information about the request initiator.
110+
/// </summary>
111+
public Initiator Initiator { get; }
112+
108113
/// <summary>
109114
/// True when the request has POST data. Note that <see cref="PostData"/> might still be null when this flag is true
110115
/// when the data is too long or not readily available in the decoded form.
@@ -117,23 +122,29 @@ public interface IRequest
117122
/// If the URL is set it won't perform a redirect. The request will be silently forwarded to the new url. For example, the address bar will show the original url.
118123
/// </summary>
119124
/// <param name="payloadOverrides">Optional request overwrites.</param>
125+
/// <param name="priority">Optional intercept abort priority. If provided, intercept will be resolved using cooperative handling rules. Otherwise, intercept will be resolved immediately.
126+
/// IMPORTANT: If you set the priority, you will need to attach Request listener using <see cref="IPage.AddRequestInterceptor"/> instead of <see cref="IPage.Request"/>.</param>
120127
/// <returns>Task.</returns>
121-
Task ContinueAsync(Payload payloadOverrides = null);
128+
Task ContinueAsync(Payload payloadOverrides = null, int? priority = null);
122129

123130
/// <summary>
124131
/// Fulfills request with given response. To use this, request interception should be enabled with <see cref="IPage.SetRequestInterceptionAsync(bool)"/>. Exception is thrown if request interception is not enabled.
125132
/// </summary>
126133
/// <param name="response">Response that will fulfill this request.</param>
134+
/// <param name="priority">Optional intercept abort priority. If provided, intercept will be resolved using cooperative handling rules. Otherwise, intercept will be resolved immediately.
135+
/// IMPORTANT: If you set the priority, you will need to attach Request listener using <see cref="IPage.AddRequestInterceptor"/> instead of <see cref="IPage.Request"/>.</param>
127136
/// <returns>Task.</returns>
128-
Task RespondAsync(ResponseData response);
137+
Task RespondAsync(ResponseData response, int? priority = null);
129138

130139
/// <summary>
131140
/// Aborts request. To use this, request interception should be enabled with <see cref="IPage.SetRequestInterceptionAsync(bool)"/>.
132141
/// Exception is immediately thrown if the request interception is not enabled.
133142
/// </summary>
134143
/// <param name="errorCode">Optional error code. Defaults to <see cref="RequestAbortErrorCode.Failed"/>.</param>
144+
/// <param name="priority">Optional intercept abort priority. If provided, intercept will be resolved using cooperative handling rules. Otherwise, intercept will be resolved immediately.
145+
/// IMPORTANT: If you set the priority, you will need to attach Request listener using <see cref="IPage.AddRequestInterceptor"/> instead of <see cref="IPage.Request"/>.</param>
135146
/// <returns>Task.</returns>
136-
Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortErrorCode.Failed);
147+
Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortErrorCode.Failed, int? priority = null);
137148

138149
/// <summary>
139150
/// Fetches the POST data for the request from the browser.

0 commit comments

Comments
 (0)