diff --git a/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs new file mode 100644 index 000000000..c6ae141c4 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs @@ -0,0 +1,787 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using PuppeteerSharp.Tests.Attributes; +using PuppeteerSharp.Nunit; +using NUnit.Framework; +using PuppeteerSharp.Helpers; + +namespace PuppeteerSharp.Tests.RequestInterceptionExperimentalTests; + +public class PageSetRequestInterceptionTests : PuppeteerPageBaseTest +{ + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should cooperatively ${expectedAction} by priority")] + [Skip(SkipAttribute.Targets.Firefox)] + [TestCase("abort")] + [TestCase("continue")] + [TestCase("respond")] + public async Task ShouldCooperativelyActByPriority(string expectedAction) + { + var actionResults = new List(); + + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + if (request.Url.EndsWith(".css")) + { + var headers = request.Headers; + headers["xaction"] = "continue"; + return request.ContinueAsync(new Payload() { Headers = headers, }, + expectedAction == "continue" ? 1 : 0); + } + + return request.ContinueAsync(new Payload(), 0); + }); + + Page.AddRequestInterceptor(request => + { + if (request.Url.EndsWith(".css")) + { + Dictionary headers = []; + foreach (var kvp in request.Headers) + { + headers.Add(kvp.Key, kvp.Value); + } + + headers["xaction"] = "respond"; + return request.RespondAsync(new ResponseData() { Headers = headers, }, + expectedAction == "respond" ? 1 : 0); + } + + return request.ContinueAsync(new Payload(), 0); + }); + + Page.AddRequestInterceptor(request => + { + if (request.Url.EndsWith(".css")) + { + var headers = request.Headers; + headers["xaction"] = "abort"; + return request.AbortAsync(RequestAbortErrorCode.Aborted, expectedAction == "abort" ? 1 : 0); + } + + return request.ContinueAsync(new Payload(), 0); + }); + + Page.Response += (_, e) => + { + e.Response.Headers.TryGetValue("xaction", out var xaction); + + if (e.Response.Url.EndsWith(".css") && !string.IsNullOrEmpty(xaction)) + { + actionResults.Add(xaction); + } + }; + + Page.RequestFailed += (_, e) => + { + if (e.Request.Url.EndsWith(".css")) + { + actionResults.Add("abort"); + } + }; + + IResponse response; + + if (expectedAction == "continue") + { + var serverRequestTask = Server.WaitForRequest("/one-style.css", request => request.Headers["xaction"]); + response = await Page.GoToAsync(TestConstants.ServerUrl + "/one-style.html"); + await serverRequestTask; + actionResults.Add(serverRequestTask.Result); + } + else + { + response = await Page.GoToAsync(TestConstants.ServerUrl + "/one-style.html"); + } + + Assert.AreEqual(1, actionResults.Count); + Assert.AreEqual(expectedAction, actionResults[0]); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", "should intercept")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldIntercept() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(async request => + { + if (TestUtils.IsFavicon(request)) + { + await request.ContinueAsync(new Payload(), 0); + return; + } + + StringAssert.Contains("empty.html", request.Url); + Assert.NotNull(request.Headers); + Assert.NotNull(request.Headers["user-agent"]); + Assert.NotNull(request.Headers["accept"]); + Assert.AreEqual(HttpMethod.Get, request.Method); + Assert.Null(request.PostData); + Assert.True(request.IsNavigationRequest); + Assert.AreEqual(ResourceType.Document, request.ResourceType); + Assert.AreEqual(Page.MainFrame, request.Frame); + Assert.AreEqual(TestConstants.AboutBlank, request.Frame.Url); + await request.ContinueAsync(new Payload(), 0); + }); + var response = await Page.GoToAsync(TestConstants.EmptyPage); + Assert.True(response.Ok); + + Assert.AreEqual(TestConstants.Port, response.RemoteAddress.Port); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work when POST is redirected with 302")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWhenPostIsRedirectedWith302() + { + Server.SetRedirect("/rredirect", "/empty.html"); + await Page.GoToAsync(TestConstants.EmptyPage); + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(async request => await request.ContinueAsync(new Payload(), 0)); + + await Page.SetContentAsync(@" +
+ +
+ "); + await Task.WhenAll( + Page.QuerySelectorAsync("form").EvaluateFunctionAsync("form => form.submit()"), + Page.WaitForNavigationAsync() + ); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work when header manipulation headers with redirect")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWhenHeaderManipulationHeadersWithRedirect() + { + Server.SetRedirect("/rredirect", "/empty.html"); + await Page.SetRequestInterceptionAsync(true); + + Page.AddRequestInterceptor(async request => + { + var headers = request.Headers.Clone(); + headers["foo"] = "bar"; + await request.ContinueAsync(new Payload { Headers = headers }, 0); + }); + + await Page.GoToAsync(TestConstants.ServerUrl + "/rrredirect"); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should be able to remove headers")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldBeAbleToRemoveHeaders() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(async request => + { + var headers = request.Headers.Clone(); + headers["foo"] = "bar"; + headers.Remove("origin"); + await request.ContinueAsync(new Payload { Headers = headers }, 0); + }); + + var requestTask = Server.WaitForRequest("/empty.html", request => request.Headers["origin"]); + await Task.WhenAll( + requestTask, + Page.GoToAsync(TestConstants.ServerUrl + "/empty.html") + ); + Assert.True(string.IsNullOrEmpty(requestTask.Result)); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should contain referer header")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldContainRefererHeader() + { + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + var requestsReadyTcs = new TaskCompletionSource(); + + Page.AddRequestInterceptor(async request => + { + if (!TestUtils.IsFavicon(request)) + { + requests.Add(request); + + if (requests.Count > 1) + { + requestsReadyTcs.TrySetResult(true); + } + } + + await request.ContinueAsync(new Payload(), 0); + }); + + await Page.GoToAsync(TestConstants.ServerUrl + "/one-style.html"); + await requestsReadyTcs.Task.WithTimeout(); + StringAssert.Contains("/one-style.css", requests[1].Url); + StringAssert.Contains("/one-style.html", requests[1].Headers["Referer"]); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should properly return navigation response when URL has cookies")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldProperlyReturnNavigationResponseWhenURLHasCookies() + { + // Setup cookie. + await Page.GoToAsync(TestConstants.EmptyPage); + await Page.SetCookieAsync(new CookieParam { Name = "foo", Value = "bar" }); + + // Setup request interception. + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + var response = await Page.ReloadAsync(); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should stop intercepting")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldStopIntercepting() + { + await Page.SetRequestInterceptionAsync(true); + + async Task EventHandler(IRequest request) + { + await request.ContinueAsync(new Payload(), 0); + Page.RemoveRequestInterceptor(EventHandler); + } + + Page.AddRequestInterceptor(EventHandler); + await Page.GoToAsync(TestConstants.EmptyPage); + await Page.SetRequestInterceptionAsync(false); + await Page.GoToAsync(TestConstants.EmptyPage); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should show custom HTTP headers")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldShowCustomHTTPHeaders() + { + await Page.SetExtraHttpHeadersAsync(new Dictionary { ["foo"] = "bar" }); + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + Assert.AreEqual("bar", request.Headers["foo"]); + return request.ContinueAsync(new Payload(), 0); + }); + var response = await Page.GoToAsync(TestConstants.EmptyPage); + Assert.True(response.Ok); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with redirect inside sync XHR")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithRedirectInsideSyncXHR() + { + await Page.GoToAsync(TestConstants.EmptyPage); + Server.SetRedirect("/logo.png", "/pptr.png"); + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + + var status = await Page.EvaluateFunctionAsync(@"async () => + { + const request = new XMLHttpRequest(); + request.open('GET', '/logo.png', false); // `false` makes the request synchronous + request.send(null); + return request.status; + }"); + Assert.AreEqual(200, status); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with custom referer headers")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithCustomRefererHeaders() + { + await Page.SetExtraHttpHeadersAsync(new Dictionary { ["referer"] = TestConstants.EmptyPage }); + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + Assert.AreEqual(TestConstants.EmptyPage, request.Headers["referer"]); + return request.ContinueAsync(new Payload(), 0); + }); + var response = await Page.GoToAsync(TestConstants.EmptyPage); + Assert.True(response.Ok); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", "should be abortable")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldBeAbortable() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + if (request.Url.EndsWith(".css")) + { + return request.AbortAsync(RequestAbortErrorCode.Failed, 0); + } + else + { + return request.ContinueAsync(new Payload(), 0); + } + }); + var failedRequests = 0; + Page.RequestFailed += (_, _) => failedRequests++; + var response = await Page.GoToAsync(TestConstants.ServerUrl + "/one-style.html"); + Assert.True(response.Ok); + Assert.Null(response.Request.Failure); + Assert.AreEqual(1, failedRequests); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should be abortable with custom error codes")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldBeAbortableWithCustomErrorCodes() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.AbortAsync(RequestAbortErrorCode.InternetDisconnected, 0)); + IRequest failedRequest = null; + Page.RequestFailed += (_, e) => failedRequest = e.Request; + + var exception = Assert.ThrowsAsync( + () => Page.GoToAsync(TestConstants.EmptyPage)); + + StringAssert.StartsWith("net::ERR_INTERNET_DISCONNECTED", exception.Message); + Assert.NotNull(failedRequest); + Assert.AreEqual("net::ERR_INTERNET_DISCONNECTED", failedRequest.Failure); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", "should send referer")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldSendReferer() + { + await Page.SetExtraHttpHeadersAsync(new Dictionary { ["referer"] = "http://google.com/" }); + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + var requestTask = Server.WaitForRequest("/grid.html", request => request.Headers["referer"].ToString()); + await Task.WhenAll( + requestTask, + Page.GoToAsync(TestConstants.ServerUrl + "/grid.html") + ); + Assert.AreEqual("http://google.com/", requestTask.Result); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should fail navigation when aborting main resource")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldFailNavigationWhenAbortingMainResource() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.AbortAsync(RequestAbortErrorCode.Failed, 0)); + var exception = Assert.ThrowsAsync( + () => Page.GoToAsync(TestConstants.EmptyPage)); + + if (TestConstants.IsChrome) + { + StringAssert.Contains("net::ERR_FAILED", exception.Message); + } + else + { + StringAssert.Contains("NS_ERROR_FAILURE", exception.Message); + } + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with redirects")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithRedirects() + { + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + Page.AddRequestInterceptor(async request => + { + await request.ContinueAsync(new Payload(), 0); + requests.Add(request); + }); + + Server.SetRedirect("/non-existing-page.html", "/non-existing-page-2.html"); + Server.SetRedirect("/non-existing-page-2.html", "/non-existing-page-3.html"); + Server.SetRedirect("/non-existing-page-3.html", "/non-existing-page-4.html"); + Server.SetRedirect("/non-existing-page-4.html", "/empty.html"); + var response = await Page.GoToAsync(TestConstants.ServerUrl + "/non-existing-page.html"); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + StringAssert.Contains("empty.html", response.Url); + Assert.AreEqual(5, requests.Count); + Assert.AreEqual(ResourceType.Document, requests[2].ResourceType); + + // Check redirect chain + var redirectChain = response.Request.RedirectChain; + Assert.AreEqual(4, redirectChain.Length); + StringAssert.Contains("/non-existing-page.html", redirectChain[0].Url); + StringAssert.Contains("/non-existing-page-3.html", redirectChain[2].Url); + + for (var i = 0; i < redirectChain.Length; ++i) + { + var request = redirectChain[i]; + Assert.True(request.IsNavigationRequest); + Assert.AreEqual(request, request.RedirectChain.ElementAt(i)); + } + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with redirects for subresources")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithRedirectsForSubresources() + { + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + Page.AddRequestInterceptor(request => + { + if (!TestUtils.IsFavicon(request)) + { + requests.Add(request); + } + + return request.ContinueAsync(new Payload(), 0); + }); + + Server.SetRedirect("/one-style.css", "/two-style.css"); + Server.SetRedirect("/two-style.css", "/three-style.css"); + Server.SetRedirect("/three-style.css", "/four-style.css"); + Server.SetRoute("/four-style.css", + async context => { await context.Response.WriteAsync("body {box-sizing: border-box; }"); }); + + var response = await Page.GoToAsync(TestConstants.ServerUrl + "/one-style.html"); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + StringAssert.Contains("one-style.html", response.Url); + Assert.AreEqual(5, requests.Count); + Assert.AreEqual(ResourceType.Document, requests[0].ResourceType); + Assert.AreEqual(ResourceType.StyleSheet, requests[1].ResourceType); + + // Check redirect chain + var redirectChain = requests[1].RedirectChain; + Assert.AreEqual(3, redirectChain.Length); + StringAssert.Contains("one-style.css", redirectChain[0].Url); + StringAssert.Contains("three-style.css", redirectChain[2].Url); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should be able to abort redirects")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldBeAbleToAbortRedirects() + { + await Page.SetRequestInterceptionAsync(true); + Server.SetRedirect("/non-existing.json", "/non-existing-2.json"); + Server.SetRedirect("/non-existing-2.json", "/simple.html"); + Page.AddRequestInterceptor(request => + { + if (request.Url.Contains("non-existing-2")) + { + return request.AbortAsync(RequestAbortErrorCode.Failed, 0); + } + + return request.ContinueAsync(new Payload(), 0); + }); + + await Page.GoToAsync(TestConstants.EmptyPage); + var result = await Page.EvaluateFunctionAsync(@"async () => { + try + { + await fetch('/non-existing.json'); + } + catch (e) + { + return e.message; + } + }"); + + if (TestConstants.IsChrome) + { + StringAssert.Contains("Failed to fetch", result); + } + else + { + StringAssert.Contains("NetworkError", result); + } + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with equal requests")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithEqualRequests() + { + await Page.GoToAsync(TestConstants.EmptyPage); + var responseCount = 1; + Server.SetRoute("/zzz", context => context.Response.WriteAsync(((responseCount++) * 11) + string.Empty)); + await Page.SetRequestInterceptionAsync(true); + + var spinner = false; + // Cancel 2nd request. + Page.AddRequestInterceptor(request => + { + if (TestUtils.IsFavicon(request)) + { + return request.ContinueAsync(new Payload(), 0); + } + + if (spinner) + { + spinner = !spinner; + return request.AbortAsync(RequestAbortErrorCode.Failed, 0); + } + + spinner = !spinner; + return request.ContinueAsync(new Payload(), 0); + }); + + var results = await Page.EvaluateExpressionAsync(@"Promise.all([ + fetch('/zzz').then(response => response.text()).catch(e => 'FAILED'), + fetch('/zzz').then(response => response.text()).catch(e => 'FAILED'), + fetch('/zzz').then(response => response.text()).catch(e => 'FAILED'), + ])"); + Assert.AreEqual(new[] { "11", "FAILED", "22" }, results); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should navigate to dataURL and fire dataURL requests")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldNavigateToDataURLAndFireDataURLRequests() + { + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + Page.AddRequestInterceptor(request => + { + requests.Add(request); + return request.ContinueAsync(new Payload(), 0); + }); + + var dataURL = "data:text/html,
yo
"; + var response = await Page.GoToAsync(dataURL); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + Assert.That(requests, Has.Exactly(1).Items); + Assert.AreEqual(dataURL, requests[0].Url); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should be able to fetch dataURL and fire dataURL requests")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldBeAbleToFetchDataURLAndFireDataURLRequests() + { + await Page.GoToAsync(TestConstants.EmptyPage); + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + Page.AddRequestInterceptor(request => + { + requests.Add(request); + return request.ContinueAsync(new Payload(), 0); + }); + var dataURL = "data:text/html,
yo
"; + var text = await Page.EvaluateFunctionAsync("url => fetch(url).then(r => r.text())", dataURL); + + Assert.AreEqual("
yo
", text); + Assert.That(requests, Has.Exactly(1).Items); + Assert.AreEqual(dataURL, requests[0].Url); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should navigate to URL with hash and fire requests without hash")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldNavigateToURLWithHashAndAndFireRequestsWithoutHash() + { + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + Page.AddRequestInterceptor(request => + { + requests.Add(request); + return request.ContinueAsync(new Payload(), 0); + }); + var response = await Page.GoToAsync(TestConstants.EmptyPage + "#hash"); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + Assert.AreEqual(TestConstants.EmptyPage, response.Url); + Assert.That(requests, Has.Exactly(1).Items); + Assert.AreEqual(TestConstants.EmptyPage, requests[0].Url); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with encoded server")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithEncodedServer() + { + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + var response = await Page.GoToAsync(TestConstants.ServerUrl + "/some nonexisting page"); + Assert.AreEqual(HttpStatusCode.NotFound, response.Status); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with badly encoded server")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithBadlyEncodedServer() + { + await Page.SetRequestInterceptionAsync(true); + Server.SetRoute("/malformed?rnd=%911", _ => Task.CompletedTask); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + var response = await Page.GoToAsync(TestConstants.ServerUrl + "/malformed?rnd=%911"); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with encoded server - 2")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithEncodedServerNegative2() + { + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + await Page.SetRequestInterceptionAsync(true); + var requests = new List(); + Page.AddRequestInterceptor(request => + { + requests.Add(request); + return request.ContinueAsync(new Payload(), 0); + }); + var response = + await Page.GoToAsync( + $"data:text/html,"); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + Assert.AreEqual(2, requests.Count); + Assert.AreEqual(HttpStatusCode.NotFound, requests[1].Response.Status); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should not throw \"Invalid Interception Id\" if the request was cancelled")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldNotThrowInvalidInterceptionIdIfTheRequestWasCancelled() + { + await Page.SetContentAsync(""); + await Page.SetRequestInterceptionAsync(true); + IRequest request = null; + var requestIntercepted = new TaskCompletionSource(); + Page.Request += (_, e) => + { + request = e.Request; + requestIntercepted.SetResult(true); + }; + + var _ = Page.QuerySelectorAsync("iframe") + .EvaluateFunctionAsync("(frame, url) => frame.src = url", TestConstants.ServerUrl); + // Wait for request interception. + await requestIntercepted.Task; + // Delete frame to cause request to be canceled. + _ = Page.QuerySelectorAsync("iframe").EvaluateFunctionAsync("frame => frame.remove()"); + await request.ContinueAsync(new Payload(), 0); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should throw if interception is not enabled")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldThrowIfInterceptionIsNotEnabled() + { + Exception exception = null; + Page.AddRequestInterceptor(async request => + { + try + { + await request.ContinueAsync(new Payload(), 0); + } + catch (Exception ex) + { + exception = ex; + } + }); + await Page.GoToAsync(TestConstants.EmptyPage); + StringAssert.Contains("Request Interception is not enabled", exception.Message); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should work with file URLs")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkWithFileURLs() + { + await Page.SetRequestInterceptionAsync(true); + var urls = new List(); + Page.AddRequestInterceptor(request => + { + urls.Add(request.Url.Split('/').Last()); + return request.ContinueAsync(new Payload(), 0); + }); + + var uri = new Uri(Path.Combine(Directory.GetCurrentDirectory(), "Assets", "one-style.html")).AbsoluteUri; + await Page.GoToAsync(uri); + Assert.AreEqual(2, urls.Count); + Assert.Contains("one-style.html", urls); + Assert.Contains("one-style.css", urls); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should not cache if cache disabled")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldNotCacheIfCacheDisabled() + { + await Page.GoToAsync(TestConstants.ServerUrl + "/cached/one-style.html"); + await Page.SetRequestInterceptionAsync(true); + await Page.SetCacheEnabledAsync(false); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + + var cached = new List(); + Page.RequestServedFromCache += (_, e) => cached.Add(e.Request); + + await Page.ReloadAsync(); + Assert.IsEmpty(cached); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should cache if cache enabled")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldNotCacheIfCacheEnabled() + { + await Page.GoToAsync(TestConstants.ServerUrl + "/cached/one-style.html"); + await Page.SetRequestInterceptionAsync(true); + await Page.SetCacheEnabledAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + + var cached = new List(); + Page.RequestServedFromCache += (_, e) => cached.Add(e.Request); + + await Page.ReloadAsync(); + Assert.That(cached, Has.Exactly(1).Items); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Page.setRequestInterception", + "should load fonts if cache enabled")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldLoadFontsIfCacheEnabled() + { + await Page.SetRequestInterceptionAsync(true); + await Page.SetCacheEnabledAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload(), 0)); + + var waitTask = Page.WaitForResponseAsync(response => response.Url.EndsWith("/one-style.woff")); + await Page.GoToAsync(TestConstants.ServerUrl + "/cached/one-style-font.html"); + await waitTask; + } +} diff --git a/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/RequestContinueTests.cs b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/RequestContinueTests.cs new file mode 100644 index 000000000..9ec627bb9 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/RequestContinueTests.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using PuppeteerSharp.Tests.Attributes; +using PuppeteerSharp.Nunit; +using NUnit.Framework; + +namespace PuppeteerSharp.Tests.RequestInterceptionExperimentalTests; + +public class RequestContinueTests : PuppeteerPageBaseTest +{ + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should work")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWork() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(async request => await request.ContinueAsync(new Payload(), 0)); + await Page.GoToAsync(TestConstants.EmptyPage); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should amend HTTP headers")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldAmendHTTPHeaders() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + var headers = new Dictionary(request.Headers) { ["FOO"] = "bar" }; + return request.ContinueAsync(new Payload { Headers = headers }, 0); + }); + await Page.GoToAsync(TestConstants.EmptyPage); + var requestTask = Server.WaitForRequest("/sleep.zzz", request => request.Headers["foo"].ToString()); + await Task.WhenAll( + requestTask, + Page.EvaluateExpressionAsync("fetch('/sleep.zzz')") + ); + Assert.AreEqual("bar", requestTask.Result); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", + "should redirect in a way non-observable to page")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldRedirectInAWayNonObservableToPage() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + var redirectURL = request.Url.Contains("/empty.html") + ? TestConstants.ServerUrl + "/consolelog.html" + : null; + return request.ContinueAsync(new Payload { Url = redirectURL }, 0); + }); + string consoleMessage = null; + Page.Console += (_, e) => consoleMessage = e.Message.Text; + await Page.GoToAsync(TestConstants.EmptyPage); + Assert.AreEqual(TestConstants.EmptyPage, Page.Url); + Assert.AreEqual("yellow", consoleMessage); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should amend method")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldAmendMethodData() + { + await Page.GoToAsync(TestConstants.EmptyPage); + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload { Method = HttpMethod.Post }, 0)); + + var requestTask = Server.WaitForRequest("/sleep.zzz", request => request.Method); + + await Task.WhenAll( + requestTask, + Page.EvaluateExpressionAsync("fetch('/sleep.zzz')") + ); + + Assert.AreEqual("POST", requestTask.Result); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", "should amend post data")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldAmendPostData() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync(new Payload { Method = HttpMethod.Post, PostData = "doggo" }, 0)); + var requestTask = Server.WaitForRequest("/sleep.zzz", async request => + { + using var reader = new StreamReader(request.Body, Encoding.UTF8); + return await reader.ReadToEndAsync(); + }); + + await Task.WhenAll( + requestTask, + Page.GoToAsync(TestConstants.ServerUrl + "/sleep.zzz") + ); + + Assert.AreEqual("doggo", await requestTask.Result); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.continue", + "should amend both post data and method on navigation")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldAmendBothPostDataAndMethodOnNavigation() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.ContinueAsync( + new Payload + { + Method = HttpMethod.Post, PostData = "doggo" + }, + 0)); + + var serverRequestTask = Server.WaitForRequest("/empty.html", async req => + { + var body = await new StreamReader(req.Body).ReadToEndAsync(); + return new { req.Method, Body = body }; + }); + + await Task.WhenAll( + serverRequestTask, + Page.GoToAsync(TestConstants.EmptyPage) + ); + var serverRequest = await serverRequestTask; + Assert.AreEqual(HttpMethod.Post.Method, serverRequest.Result.Method); + Assert.AreEqual("doggo", serverRequest.Result.Body); + } +} diff --git a/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/RequestRespondTests.cs b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/RequestRespondTests.cs new file mode 100644 index 000000000..fc7f609a0 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/RequestRespondTests.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using PuppeteerSharp.Tests.Attributes; +using PuppeteerSharp.Nunit; +using NUnit.Framework; + +namespace PuppeteerSharp.Tests.RequestInterceptionExperimentalTests; + +public class RequestRespondTests : PuppeteerPageBaseTest +{ + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should work")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWork() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.RespondAsync(new ResponseData + { + Status = HttpStatusCode.Created, + Headers = new Dictionary { ["foo"] = "bar" }, + Body = "Yo, page!" + }, 0)); + + var response = await Page.GoToAsync(TestConstants.EmptyPage); + Assert.AreEqual(HttpStatusCode.Created, response.Status); + Assert.AreEqual("bar", response.Headers["foo"]); + Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync("document.body.textContent")); + } + + /// + /// In puppeteer this method is called ShouldWorkWithStatusCode422. + /// 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) + /// 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 + /// + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should work with status code 422")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldWorkReturnStatusPhrases() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.RespondAsync(new ResponseData + { + Status = HttpStatusCode.UpgradeRequired, Body = "Yo, page!" + }, 0)); + + var response = await Page.GoToAsync(TestConstants.EmptyPage); + Assert.AreEqual(HttpStatusCode.UpgradeRequired, response.Status); + Assert.AreEqual("Upgrade Required", response.StatusText); + Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync("document.body.textContent")); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should redirect")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldRedirect() + { + await Page.SetRequestInterceptionAsync(true); + + Page.AddRequestInterceptor(request => + { + if (!request.Url.Contains("rrredirect")) + { + return request.ContinueAsync(new Payload(), 0); + } + + return request.RespondAsync(new ResponseData + { + Status = HttpStatusCode.Redirect, + Headers = new Dictionary { ["location"] = TestConstants.EmptyPage } + }, 0); + }); + + var response = await Page.GoToAsync(TestConstants.ServerUrl + "/rrredirect"); + + Assert.That(response.Request.RedirectChain, Has.Exactly(1).Items); + Assert.AreEqual(TestConstants.ServerUrl + "/rrredirect", response.Request.RedirectChain[0].Url); + Assert.AreEqual(TestConstants.EmptyPage, response.Url); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", "should allow mocking binary responses")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldAllowMockingBinaryResponses() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + var imageData = File.ReadAllBytes("./Assets/pptr.png"); + return request.RespondAsync(new ResponseData { ContentType = "image/png", BodyData = imageData }, 0); + }); + + await Page.EvaluateFunctionAsync(@"PREFIX => + { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise(fulfill => img.onload = fulfill); + }", TestConstants.ServerUrl); + var img = await Page.QuerySelectorAsync("img"); + Assert.True(ScreenshotHelper.PixelMatch("mock-binary-response.png", await img.ScreenshotDataAsync())); + } + + [PuppeteerTest("requestinterception-experimental.spec.ts", "Request.respond", + "should stringify intercepted request response headers")] + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldStringifyInterceptedRequestResponseHeaders() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => request.RespondAsync(new ResponseData + { + Status = HttpStatusCode.OK, + Headers = new Dictionary { ["foo"] = true }, + Body = "Yo, page!" + }, 0)); + + var response = await Page.GoToAsync(TestConstants.EmptyPage); + Assert.AreEqual(HttpStatusCode.OK, response.Status); + Assert.AreEqual("True", response.Headers["foo"]); + Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync("document.body.textContent")); + } + + [Skip(SkipAttribute.Targets.Firefox)] + public async Task ShouldAllowMultipleInterceptedRequestResponseHeaders() + { + await Page.SetRequestInterceptionAsync(true); + Page.AddRequestInterceptor(request => + { + return request.RespondAsync(new ResponseData + { + Status = HttpStatusCode.OK, + Headers = new Dictionary + { + ["foo"] = new [] { true, false }, + ["Set-Cookie"] = new [] { "sessionId=abcdef", "specialId=123456" } + }, + Body = "Yo, page!" + }, 0); + }); + + var response = await Page.GoToAsync(TestConstants.EmptyPage); + var cookies = await Page.GetCookiesAsync(TestConstants.EmptyPage); + + Assert.AreEqual(HttpStatusCode.OK, response.Status); + Assert.AreEqual("True\nFalse", response.Headers["foo"]); + Assert.AreEqual("Yo, page!", await Page.EvaluateExpressionAsync("document.body.textContent")); + Assert.AreEqual("specialId", cookies[0].Name); + Assert.AreEqual("123456", cookies[0].Value); + Assert.AreEqual("sessionId", cookies[1].Name); + Assert.AreEqual("abcdef", cookies[1].Value); + } +} diff --git a/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs b/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs index 4d055cdaf..db5693c64 100644 --- a/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs +++ b/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs @@ -15,10 +15,6 @@ namespace PuppeteerSharp.Tests.RequestInterceptionTests { public class SetRequestInterceptionTests : PuppeteerPageBaseTest { - public SetRequestInterceptionTests(): base() - { - } - [PuppeteerTest("requestinterception.spec.ts", "Page.setRequestInterception", "should intercept")] [Skip(SkipAttribute.Targets.Firefox)] public async Task ShouldIntercept() @@ -51,7 +47,7 @@ public async Task ShouldIntercept() [PuppeteerTest("requestinterception.spec.ts", "Page.setRequestInterception", "should work when POST is redirected with 302")] [Skip(SkipAttribute.Targets.Firefox)] - public async Task ShouldWorkWhenPostIsEedirectedWith302() + public async Task ShouldWorkWhenPostIsRedirectedWith302() { Server.SetRedirect("/rredirect", "/empty.html"); await Page.GoToAsync(TestConstants.EmptyPage); diff --git a/lib/PuppeteerSharp.Tests/Screenshots/golden-chromium/test.png b/lib/PuppeteerSharp.Tests/Screenshots/golden-chromium/test.png index 873db9a07..f91bc4285 100644 Binary files a/lib/PuppeteerSharp.Tests/Screenshots/golden-chromium/test.png and b/lib/PuppeteerSharp.Tests/Screenshots/golden-chromium/test.png differ diff --git a/lib/PuppeteerSharp/BrowserData/Firefox.cs b/lib/PuppeteerSharp/BrowserData/Firefox.cs index 6a8e50464..64c64d51f 100644 --- a/lib/PuppeteerSharp/BrowserData/Firefox.cs +++ b/lib/PuppeteerSharp/BrowserData/Firefox.cs @@ -15,7 +15,7 @@ public static class Firefox /// public const string DefaultBuildId = "FIREFOX_NIGHTLY"; - private static readonly Dictionary _cachedBuildIds = new(); + private static readonly Dictionary _cachedBuildIds = []; internal static Task GetDefaultBuildIdAsync() => ResolveBuildIdAsync(DefaultBuildId); diff --git a/lib/PuppeteerSharp/IPage.cs b/lib/PuppeteerSharp/IPage.cs index 7f2a2fa7b..bfd81b49e 100644 --- a/lib/PuppeteerSharp/IPage.cs +++ b/lib/PuppeteerSharp/IPage.cs @@ -1098,8 +1098,8 @@ public interface IPage : IDisposable, IAsyncDisposable Task SetOfflineModeAsync(bool value); /// - /// Activating request interception enables request.AbortAsync, - /// request.ContinueAsync and request.RespondAsync methods. + /// Activating request interception enables request.AbortAsync, + /// request.ContinueAsync and request.RespondAsync methods. /// /// The request interception task. /// Whether to enable request interception.. @@ -1438,5 +1438,19 @@ public interface IPage : IDisposable, IAsyncDisposable /// Optional waiting parameters. /// A task that resolves after the page gets the prompt. Task WaitForDevicePromptAsync(WaitTimeoutOptions options = null); + + /// + /// , , and can accept an optional `priority` to activate Cooperative Intercept Mode. + /// In Cooperative Mode, all interception tasks are guaranteed to run and all async handlers are awaited. + /// The interception is resolved to the highest-priority resolution. + /// + /// Interception task. + void AddRequestInterceptor(Func interceptionTask); + + /// + /// Removes a previously added request interceptor. + /// + /// Interception task. + void RemoveRequestInterceptor(Func interceptionTask); } } diff --git a/lib/PuppeteerSharp/IRequest.cs b/lib/PuppeteerSharp/IRequest.cs index 7dffe63c2..7b7f363a0 100644 --- a/lib/PuppeteerSharp/IRequest.cs +++ b/lib/PuppeteerSharp/IRequest.cs @@ -105,6 +105,11 @@ public interface IRequest /// The redirect chain. IRequest[] RedirectChain { get; } + /// + /// Information about the request initiator. + /// + public Initiator Initiator { get; } + /// /// True when the request has POST data. Note that might still be null when this flag is true /// when the data is too long or not readily available in the decoded form. @@ -117,23 +122,29 @@ public interface IRequest /// 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. /// /// Optional request overwrites. + /// Optional intercept abort priority. If provided, intercept will be resolved using cooperative handling rules. Otherwise, intercept will be resolved immediately. + /// IMPORTANT: If you set the priority, you will need to attach Request listener using instead of . /// Task. - Task ContinueAsync(Payload payloadOverrides = null); + Task ContinueAsync(Payload payloadOverrides = null, int? priority = null); /// /// Fulfills request with given response. To use this, request interception should be enabled with . Exception is thrown if request interception is not enabled. /// /// Response that will fulfill this request. + /// Optional intercept abort priority. If provided, intercept will be resolved using cooperative handling rules. Otherwise, intercept will be resolved immediately. + /// IMPORTANT: If you set the priority, you will need to attach Request listener using instead of . /// Task. - Task RespondAsync(ResponseData response); + Task RespondAsync(ResponseData response, int? priority = null); /// /// Aborts request. To use this, request interception should be enabled with . /// Exception is immediately thrown if the request interception is not enabled. /// /// Optional error code. Defaults to . + /// Optional intercept abort priority. If provided, intercept will be resolved using cooperative handling rules. Otherwise, intercept will be resolved immediately. + /// IMPORTANT: If you set the priority, you will need to attach Request listener using instead of . /// Task. - Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortErrorCode.Failed); + Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortErrorCode.Failed, int? priority = null); /// /// Fetches the POST data for the request from the browser. diff --git a/lib/PuppeteerSharp/Initiator.cs b/lib/PuppeteerSharp/Initiator.cs new file mode 100644 index 000000000..b6e92ec8d --- /dev/null +++ b/lib/PuppeteerSharp/Initiator.cs @@ -0,0 +1,54 @@ +// * MIT License +// * +// * Copyright (c) 2020 Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +namespace PuppeteerSharp; + +/// +/// Information about the request initiator. +/// +public class Initiator +{ + /// + /// Gets or sets the type of the initiator. + /// + public InitiatorType Type { get; set; } + + /// + /// Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type. + /// + public string Url { get; set; } + + /// + /// Initiator line number, set for Parser type or for Script type (when script is importing module) (0-based). + /// + public int? LineNumber { get; set; } + + /// + /// Initiator column number, set for Parser type or for Script type (when script is importing module) (0-based). + /// + public int? ColumnNumber { get; set; } + + /// + /// Set if another request triggered this request (e.g. preflight). + /// + public string RequestId { get; set; } +} diff --git a/lib/PuppeteerSharp/InitiatorType.cs b/lib/PuppeteerSharp/InitiatorType.cs new file mode 100644 index 000000000..f12aee3bd --- /dev/null +++ b/lib/PuppeteerSharp/InitiatorType.cs @@ -0,0 +1,40 @@ +using System.Runtime.Serialization; + +namespace PuppeteerSharp; + +/// +/// Type of the . +/// +public enum InitiatorType +{ + /// + /// Parser. + /// + Parser, + + /// + /// Script. + /// + Script, + + /// + /// Preload. + /// + Preload, + + /// + /// SignedExchange. + /// + [EnumMember(Value = "SignedExchange")] + SignedExchange, + + /// + /// Preflight. + /// + Preflight, + + /// + /// Other. + /// + Other, +} diff --git a/lib/PuppeteerSharp/InterceptResolutionAction.cs b/lib/PuppeteerSharp/InterceptResolutionAction.cs new file mode 100644 index 000000000..2b37b7009 --- /dev/null +++ b/lib/PuppeteerSharp/InterceptResolutionAction.cs @@ -0,0 +1,33 @@ +// * MIT License +// * +// * Copyright (c) 2020 Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +namespace PuppeteerSharp; + +internal enum InterceptResolutionAction +{ + Abort, + Respond, + Continue, + Disabled, + None, + AlreadyHandled, +} diff --git a/lib/PuppeteerSharp/InterceptResolutionState.cs b/lib/PuppeteerSharp/InterceptResolutionState.cs new file mode 100644 index 000000000..fbb5ad487 --- /dev/null +++ b/lib/PuppeteerSharp/InterceptResolutionState.cs @@ -0,0 +1,30 @@ +// * MIT License +// * +// * Copyright (c) 2020 Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +namespace PuppeteerSharp; + +internal class InterceptResolutionState(InterceptResolutionAction action, int? priority = null) +{ + public InterceptResolutionAction Action { get; set; } = action; + + public int? Priority { get; set; } = priority; +} diff --git a/lib/PuppeteerSharp/Messaging/RequestWillBeSentPayload.cs b/lib/PuppeteerSharp/Messaging/RequestWillBeSentPayload.cs index 57a705e1a..55e15b32b 100644 --- a/lib/PuppeteerSharp/Messaging/RequestWillBeSentPayload.cs +++ b/lib/PuppeteerSharp/Messaging/RequestWillBeSentPayload.cs @@ -1,3 +1,5 @@ +using PuppeteerSharp.Messaging.Protocol.Network; + namespace PuppeteerSharp.Messaging { internal class RequestWillBeSentPayload @@ -10,10 +12,12 @@ internal class RequestWillBeSentPayload public ResponsePayload RedirectResponse { get; set; } - public ResourceType Type { get; set; } + public ResourceType? Type { get; set; } public string FrameId { get; set; } public bool RedirectHasExtraInfo { get; set; } + + public Initiator Initiator { get; set; } } } diff --git a/lib/PuppeteerSharp/Messaging/StackTrace.cs b/lib/PuppeteerSharp/Messaging/StackTrace.cs index 11e9e9856..9e9dabc4d 100644 --- a/lib/PuppeteerSharp/Messaging/StackTrace.cs +++ b/lib/PuppeteerSharp/Messaging/StackTrace.cs @@ -2,6 +2,12 @@ namespace PuppeteerSharp.Messaging { internal class StackTrace { + public string Description { get; set; } + public ConsoleMessageLocation[] CallFrames { get; set; } + + public StackTrace Parent { get; set; } + + public string ParentId { get; set; } } } diff --git a/lib/PuppeteerSharp/NetworkManager.cs b/lib/PuppeteerSharp/NetworkManager.cs index a52980b80..33c47c8ae 100644 --- a/lib/PuppeteerSharp/NetworkManager.cs +++ b/lib/PuppeteerSharp/NetworkManager.cs @@ -242,10 +242,7 @@ private void EmitLoadingFailed(LoadingFailedEventResponse e) ForgetRequest(request, true); - RequestFailed?.Invoke(this, new RequestEventArgs - { - Request = request, - }); + RequestFailed?.Invoke(this, new RequestEventArgs(request)); } private void OnLoadingFinished(LoadingFinishedEventResponse e) @@ -274,10 +271,7 @@ private void EmitLoadingFinished(LoadingFinishedEventResponse e) ForgetRequest(request, true); - RequestFinished?.Invoke(this, new RequestEventArgs - { - Request = request, - }); + RequestFinished?.Invoke(this, new RequestEventArgs(request)); } private void ForgetRequest(Request request, bool events) @@ -335,10 +329,7 @@ private void EmitResponseEvent(ResponseReceivedResponse e, ResponseReceivedExtra request.Response = response; - Response?.Invoke(this, new ResponseCreatedEventArgs - { - Response = response, - }); + Response?.Invoke(this, new ResponseCreatedEventArgs(response)); } private async Task OnAuthRequiredAsync(FetchAuthRequiredResponse e) @@ -463,10 +454,16 @@ private async Task OnRequestAsync(RequestWillBeSentPayload e, string fetchReques _networkEventManager.StoreRequest(e.RequestId, request); - Request?.Invoke(this, new RequestEventArgs + Request?.Invoke(this, new RequestEventArgs(request)); + + try { - Request = request, - }); + await request.FinalizeInterceptionsAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to FinalizeInterceptionsAsync"); + } } private void OnRequestServedFromCache(RequestServedFromCacheResponse response) @@ -478,7 +475,7 @@ private void OnRequestServedFromCache(RequestServedFromCacheResponse response) request.FromMemoryCache = true; } - RequestServedFromCache?.Invoke(this, new RequestEventArgs { Request = request }); + RequestServedFromCache?.Invoke(this, new RequestEventArgs(request)); } private void HandleRequestRedirect(Request request, ResponsePayload responseMessage, ResponseReceivedExtraInfoResponse extraInfo) @@ -496,15 +493,8 @@ private void HandleRequestRedirect(Request request, ResponsePayload responseMess ForgetRequest(request, false); - Response?.Invoke(this, new ResponseCreatedEventArgs - { - Response = response, - }); - - RequestFinished?.Invoke(this, new RequestEventArgs - { - Request = request, - }); + Response?.Invoke(this, new ResponseCreatedEventArgs(response)); + RequestFinished?.Invoke(this, new RequestEventArgs(request)); } private async Task OnRequestWillBeSentAsync(RequestWillBeSentPayload e) diff --git a/lib/PuppeteerSharp/Page.cs b/lib/PuppeteerSharp/Page.cs index 8fd3ca59e..8a00d3357 100644 --- a/lib/PuppeteerSharp/Page.cs +++ b/lib/PuppeteerSharp/Page.cs @@ -60,6 +60,7 @@ public class Page : IPage private readonly TaskCompletionSource _closeCompletedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TimeoutSettings _timeoutSettings = new(); private readonly ConcurrentDictionary> _fileChooserInterceptors = new(); + private readonly ConcurrentSet> _requestInterceptionTask = []; private PageGetLayoutMetricsResponse _burstModeMetrics; private bool _screenshotBurstModeOn; private ScreenshotOptions _screenshotBurstModeOptions; @@ -1143,6 +1144,14 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + public void AddRequestInterceptor(Func interceptionTask) + => _requestInterceptionTask.Add(interceptionTask); + + /// + public void RemoveRequestInterceptor(Func interceptionTask) + => _requestInterceptionTask.Remove(interceptionTask); + internal static async Task CreateAsync( CDPSession client, Target target, @@ -1253,7 +1262,7 @@ private async Task InitializeAsync() FrameManager.FrameDetached += (_, e) => FrameDetached?.Invoke(this, e); FrameManager.FrameNavigated += (_, e) => FrameNavigated?.Invoke(this, e); - networkManager.Request += (_, e) => Request?.Invoke(this, e); + networkManager.Request += (_, e) => OnRequest(e.Request); networkManager.RequestFailed += (_, e) => RequestFailed?.Invoke(this, e); networkManager.Response += (_, e) => Response?.Invoke(this, e); networkManager.RequestFinished += (_, e) => RequestFinished?.Invoke(this, e); @@ -1264,6 +1273,22 @@ await Task.WhenAll( Client.SendAsync("Log.enable")).ConfigureAwait(false); } + private void OnRequest(IRequest request) + { + if (request == null) + { + return; + } + + // Run tasks one after the other + foreach (var subscriber in _requestInterceptionTask) + { + (request as Request)?.EnqueueInterceptionAction(subscriber); + } + + Request?.Invoke(this, new RequestEventArgs(request)); + } + private async Task GoAsync(int delta, NavigationOptions options) { var history = await Client.SendAsync("Page.getNavigationHistory").ConfigureAwait(false); diff --git a/lib/PuppeteerSharp/Request.cs b/lib/PuppeteerSharp/Request.cs index 1b0f7fee3..53f87ae0c 100644 --- a/lib/PuppeteerSharp/Request.cs +++ b/lib/PuppeteerSharp/Request.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Http; @@ -18,39 +19,41 @@ public class Request : IRequest private readonly CDPSession _client; private readonly bool _allowInterception; private readonly ILogger _logger; - private bool _interceptionHandled; + private readonly List> _interceptHandlers = []; + private Payload _continueRequestOverrides = new(); + private ResponseData _responseForRequest; + private RequestAbortErrorCode _abortErrorReason; + private InterceptResolutionState _interceptResolutionState = new(InterceptResolutionAction.None); internal Request( CDPSession client, - Frame frame, + IFrame frame, string interceptionId, bool allowInterception, - RequestWillBeSentPayload e, + RequestWillBeSentPayload data, List redirectChain) { _client = client; - _allowInterception = allowInterception; - _interceptionHandled = false; _logger = _client.Connection.LoggerFactory.CreateLogger(); - - RequestId = e.RequestId; + RequestId = data.RequestId; + IsNavigationRequest = data.RequestId == data.LoaderId && data.Type == ResourceType.Document; InterceptionId = interceptionId; - IsNavigationRequest = e.RequestId == e.LoaderId && e.Type == ResourceType.Document; - Url = e.Request.Url; - ResourceType = e.Type; - Method = e.Request.Method; - PostData = e.Request.PostData; - HasPostData = e.Request.HasPostData ?? false; + _allowInterception = allowInterception; + Url = data.Request.Url; + ResourceType = data.Type ?? ResourceType.Other; + Method = data.Request.Method; + PostData = data.Request.PostData; + HasPostData = data.Request.HasPostData ?? false; + Frame = frame; RedirectChainList = redirectChain; + Initiator = data.Initiator; Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var keyValue in e.Request.Headers) + foreach (var keyValue in data.Request.Headers) { Headers[keyValue.Key] = keyValue.Value; } - - FromMemoryCache = false; } /// @@ -92,15 +95,74 @@ internal Request( /// public IRequest[] RedirectChain => RedirectChainList.ToArray(); - /// - public bool HasPostData { get; private set; } + /// + public Initiator Initiator { get; } - internal bool FromMemoryCache { get; set; } + /// + public bool HasPostData { get; } internal List RedirectChainList { get; } + internal Payload ContinueRequestOverrides + { + get + { + if (!_allowInterception) + { + throw new PuppeteerException("Request Interception is not enabled!"); + } + + return _continueRequestOverrides; + } + } + + internal ResponseData ResponseForRequest + { + get + { + if (!_allowInterception) + { + throw new PuppeteerException("Request Interception is not enabled!"); + } + + return _responseForRequest; + } + } + + internal RequestAbortErrorCode AbortErrorReason + { + get + { + if (!_allowInterception) + { + throw new PuppeteerException("Request Interception is not enabled!"); + } + + return _abortErrorReason; + } + } + + internal bool FromMemoryCache { get; set; } + + private InterceptResolutionState InterceptResolutionState + { + get + { + if (!_allowInterception) + { + return new InterceptResolutionState(InterceptResolutionAction.Disabled); + } + + return IsInterceptResolutionHandled + ? new InterceptResolutionState(InterceptResolutionAction.AlreadyHandled) + : _interceptResolutionState; + } + } + + private bool IsInterceptResolutionHandled { get; set; } + /// - public async Task ContinueAsync(Payload overrides = null) + public async Task ContinueAsync(Payload overrides = null, int? priority = null) { // Request interception is not supported for data: urls. if (Url.StartsWith("data:", StringComparison.InvariantCultureIgnoreCase)) @@ -113,12 +175,166 @@ public async Task ContinueAsync(Payload overrides = null) throw new PuppeteerException("Request Interception is not enabled!"); } - if (_interceptionHandled) + if (IsInterceptResolutionHandled) { throw new PuppeteerException("Request is already handled!"); } - _interceptionHandled = true; + if (priority is null) + { + await ContinueInternalAsync(overrides).ConfigureAwait(false); + return; + } + + _continueRequestOverrides = overrides; + + if (_interceptResolutionState.Priority is null || priority > _interceptResolutionState.Priority) + { + _interceptResolutionState = new InterceptResolutionState(InterceptResolutionAction.Continue, priority); + return; + } + + if (priority == _interceptResolutionState.Priority) + { + if (_interceptResolutionState.Action == InterceptResolutionAction.Abort || + _interceptResolutionState.Action == InterceptResolutionAction.Respond) + { + return; + } + + _interceptResolutionState.Action = InterceptResolutionAction.Continue; + } + } + + /// + public async Task RespondAsync(ResponseData response, int? priority = null) + { + if (Url.StartsWith("data:", StringComparison.Ordinal)) + { + return; + } + + if (!_allowInterception) + { + throw new PuppeteerException("Request Interception is not enabled!"); + } + + if (IsInterceptResolutionHandled) + { + throw new PuppeteerException("Request is already handled!"); + } + + if (priority is null) + { + Debug.Assert(response != null, nameof(response) + " != null"); + await RespondInternalAsync(response).ConfigureAwait(false); + } + + _responseForRequest = response; + + if (_interceptResolutionState.Priority is null || priority > _interceptResolutionState.Priority) + { + _interceptResolutionState = new InterceptResolutionState(InterceptResolutionAction.Respond, priority); + return; + } + + if (priority == _interceptResolutionState.Priority) + { + if (_interceptResolutionState.Action == InterceptResolutionAction.Abort) + { + return; + } + + _interceptResolutionState.Action = InterceptResolutionAction.Respond; + } + } + + /// + public async Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortErrorCode.Failed, int? priority = null) + { + // Request interception is not supported for data: urls. + if (Url.StartsWith("data:", StringComparison.InvariantCultureIgnoreCase)) + { + return; + } + + if (!_allowInterception) + { + throw new PuppeteerException("Request Interception is not enabled!"); + } + + if (IsInterceptResolutionHandled) + { + throw new PuppeteerException("Request is already handled!"); + } + + if (priority is null) + { + await AbortInternalAsync(errorCode).ConfigureAwait(false); + return; + } + + _abortErrorReason = errorCode; + + if (_interceptResolutionState.Priority is null || priority > _interceptResolutionState.Priority) + { + _interceptResolutionState = new InterceptResolutionState(InterceptResolutionAction.Abort, priority); + } + } + + /// + public async Task FetchPostDataAsync() + { + try + { + var result = await _client.SendAsync( + "Network.getRequestPostData", + new GetRequestPostDataRequest(RequestId)).ConfigureAwait(false); + return result.PostData; + } + catch (Exception ex) + { + _logger.LogError(ex, ex.ToString()); + } + + return null; + } + + internal async Task FinalizeInterceptionsAsync() + { + foreach (var handler in _interceptHandlers) + { + await handler(this).ConfigureAwait(false); + } + + switch (InterceptResolutionState.Action) + { + case InterceptResolutionAction.Abort: + await AbortAsync(_abortErrorReason).ConfigureAwait(false); + return; + case InterceptResolutionAction.Respond: + if (_responseForRequest is null) + { + throw new PuppeteerException("Response is missing for the interception"); + } + + await RespondAsync(_responseForRequest).ConfigureAwait(false); + return; + case InterceptResolutionAction.Continue: + await ContinueInternalAsync(_continueRequestOverrides).ConfigureAwait(false); + break; + } + } + + internal void EnqueueInterceptionAction(Func pendingHandler) + => _interceptHandlers.Add(pendingHandler); + + private Header[] HeadersArray(Dictionary headers) + => headers?.Select(pair => new Header { Name = pair.Key, Value = pair.Value }).ToArray(); + + private async Task ContinueInternalAsync(Payload overrides = null) + { + IsInterceptResolutionHandled = true; try { @@ -138,7 +354,7 @@ public async Task ContinueAsync(Payload overrides = null) if (overrides?.PostData != null) { - requestData.PostData = Convert.ToBase64String(Encoding.UTF8.GetBytes(overrides?.PostData)); + requestData.PostData = Convert.ToBase64String(Encoding.UTF8.GetBytes(overrides.PostData)); } if (overrides?.Headers?.Count > 0) @@ -150,31 +366,39 @@ public async Task ContinueAsync(Payload overrides = null) } catch (PuppeteerException ex) { + IsInterceptResolutionHandled = false; + // In certain cases, protocol will return error if the request was already canceled // or the page was closed. We should tolerate these errors _logger.LogError(ex.ToString()); } } - /// - public async Task RespondAsync(ResponseData response) + private async Task AbortInternalAsync(RequestAbortErrorCode errorCode) { - if (Url.StartsWith("data:", StringComparison.Ordinal)) - { - return; - } + var errorReason = errorCode.ToString(); + IsInterceptResolutionHandled = true; - if (!_allowInterception) + try { - throw new PuppeteerException("Request Interception is not enabled!"); + await _client.SendAsync("Fetch.failRequest", new FetchFailRequest + { + RequestId = InterceptionId, + ErrorReason = errorReason, + }).ConfigureAwait(false); } - - if (_interceptionHandled) + catch (PuppeteerException ex) { - throw new PuppeteerException("Request is already handled!"); + // In certain cases, protocol will return error if the request was already canceled + // or the page was closed. We should tolerate these errors + _logger.LogError(ex.ToString()); + IsInterceptResolutionHandled = false; } + } - _interceptionHandled = true; + private async Task RespondInternalAsync(ResponseData response) + { + IsInterceptResolutionHandled = true; var responseHeaders = new List
(); @@ -211,53 +435,19 @@ public async Task RespondAsync(ResponseData response) responseHeaders.Add(new Header { Name = "content-type", Value = response.ContentType }); } - try + if (string.IsNullOrEmpty(InterceptionId)) { - await _client.SendAsync("Fetch.fulfillRequest", new FetchFulfillRequest - { - RequestId = InterceptionId, - ResponseCode = response.Status != null ? (int)response.Status : 200, - ResponseHeaders = responseHeaders.ToArray(), - Body = response.BodyData != null ? Convert.ToBase64String(response.BodyData) : null, - }).ConfigureAwait(false); - } - catch (PuppeteerException ex) - { - // In certain cases, protocol will return error if the request was already canceled - // or the page was closed. We should tolerate these errors - _logger.LogError(ex.ToString()); - } - } - - /// - public async Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortErrorCode.Failed) - { - // Request interception is not supported for data: urls. - if (Url.StartsWith("data:", StringComparison.InvariantCultureIgnoreCase)) - { - return; - } - - if (!_allowInterception) - { - throw new PuppeteerException("Request Interception is not enabled!"); - } - - if (_interceptionHandled) - { - throw new PuppeteerException("Request is already handled!"); + throw new PuppeteerException("HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest"); } - var errorReason = errorCode.ToString(); - - _interceptionHandled = true; - try { - await _client.SendAsync("Fetch.failRequest", new FetchFailRequest + await _client.SendAsync("Fetch.fulfillRequest", new FetchFulfillRequest { RequestId = InterceptionId, - ErrorReason = errorReason, + ResponseCode = response.Status != null ? (int)response.Status : 200, + ResponseHeaders = [.. responseHeaders], + Body = response.BodyData != null ? Convert.ToBase64String(response.BodyData) : null, }).ConfigureAwait(false); } catch (PuppeteerException ex) @@ -265,28 +455,8 @@ public async Task AbortAsync(RequestAbortErrorCode errorCode = RequestAbortError // In certain cases, protocol will return error if the request was already canceled // or the page was closed. We should tolerate these errors _logger.LogError(ex.ToString()); + IsInterceptResolutionHandled = false; } } - - /// - public async Task FetchPostDataAsync() - { - try - { - var result = await _client.SendAsync( - "Network.getRequestPostData", - new GetRequestPostDataRequest(RequestId)).ConfigureAwait(false); - return result.PostData; - } - catch (Exception ex) - { - _logger.LogError(ex, ex.ToString()); - } - - return null; - } - - private Header[] HeadersArray(Dictionary headers) - => headers?.Select(pair => new Header { Name = pair.Key, Value = pair.Value }).ToArray(); } } diff --git a/lib/PuppeteerSharp/RequestAbortErrorCode.cs b/lib/PuppeteerSharp/RequestAbortErrorCode.cs index cac5b3cfb..1e9d1f57f 100644 --- a/lib/PuppeteerSharp/RequestAbortErrorCode.cs +++ b/lib/PuppeteerSharp/RequestAbortErrorCode.cs @@ -1,7 +1,7 @@ namespace PuppeteerSharp { /// - /// Abort error codes. used by . + /// Abort error codes. used by . /// public enum RequestAbortErrorCode { diff --git a/lib/PuppeteerSharp/RequestEventArgs.cs b/lib/PuppeteerSharp/RequestEventArgs.cs index b03ad8ecb..bd84b121f 100644 --- a/lib/PuppeteerSharp/RequestEventArgs.cs +++ b/lib/PuppeteerSharp/RequestEventArgs.cs @@ -8,12 +8,12 @@ namespace PuppeteerSharp /// /// /// - public class RequestEventArgs : EventArgs + public class RequestEventArgs(IRequest request) : EventArgs { /// /// Gets the request. /// /// The request. - public IRequest Request { get; internal set; } + public IRequest Request { get; } = request; } } diff --git a/lib/PuppeteerSharp/ResponseCreatedEventArgs.cs b/lib/PuppeteerSharp/ResponseCreatedEventArgs.cs index 17b431da7..981f8daf9 100644 --- a/lib/PuppeteerSharp/ResponseCreatedEventArgs.cs +++ b/lib/PuppeteerSharp/ResponseCreatedEventArgs.cs @@ -5,12 +5,12 @@ namespace PuppeteerSharp /// /// arguments. /// - public class ResponseCreatedEventArgs : EventArgs + public class ResponseCreatedEventArgs(IResponse response) : EventArgs { /// /// Gets the response. /// /// The response. - public IResponse Response { get; internal set; } + public IResponse Response { get; } = response; } } diff --git a/lib/PuppeteerSharp/ResponseData.cs b/lib/PuppeteerSharp/ResponseData.cs index fa72dc3ad..9e34915bf 100644 --- a/lib/PuppeteerSharp/ResponseData.cs +++ b/lib/PuppeteerSharp/ResponseData.cs @@ -7,7 +7,7 @@ namespace PuppeteerSharp /// /// Response that will fulfill a request. /// - public struct ResponseData + public class ResponseData { /// /// Response body (text content). diff --git a/lib/PuppeteerSharp/TaskManager.cs b/lib/PuppeteerSharp/TaskManager.cs index 49e5beff3..d697f3c95 100644 --- a/lib/PuppeteerSharp/TaskManager.cs +++ b/lib/PuppeteerSharp/TaskManager.cs @@ -16,7 +16,7 @@ internal void RerunAll() { foreach (var waitTask in WaitTasks) { - _ = waitTask.Rerun(); + _ = waitTask.RerunAsync(); } } diff --git a/lib/PuppeteerSharp/WaitTask.cs b/lib/PuppeteerSharp/WaitTask.cs index e43ebc77e..3be4e77c6 100644 --- a/lib/PuppeteerSharp/WaitTask.cs +++ b/lib/PuppeteerSharp/WaitTask.cs @@ -58,7 +58,7 @@ internal WaitTask( TaskScheduler.Default); } - _ = Rerun(); + _ = RerunAsync(); } internal Task Task => _result.Task; @@ -80,7 +80,7 @@ public void Dispose() _isDisposed = true; } - internal async Task Rerun() + internal async Task RerunAsync() { try { @@ -179,7 +179,7 @@ await poller.EvaluateFunctionAsync(@"async poller => { await poller.stop(); }").ConfigureAwait(false); - poller = null; + _poller = null; } catch (Exception) { @@ -221,6 +221,13 @@ private Exception GetBadException(Exception exception) return null; } + // This is a different message coming from Firefox in the same situation. + // This is not upstream. + if (exception.Message.Contains("Could not find object with given id")) + { + return null; + } + return exception; }