diff --git a/src/PlaywrightNative.Tests/LibraryBrowserContextProxyParityTests.cs b/src/PlaywrightNative.Tests/LibraryBrowserContextProxyParityTests.cs
index d5ca0f0f..1bb00df9 100644
--- a/src/PlaywrightNative.Tests/LibraryBrowserContextProxyParityTests.cs
+++ b/src/PlaywrightNative.Tests/LibraryBrowserContextProxyParityTests.cs
@@ -29,10 +29,7 @@ namespace PlaywrightNative.Tests
{
///
/// Official library/browsercontext-proxy.spec.ts parity.
- /// Skipped (Node-only channel type validation):
- /// should throw for bad server value (proxy.server: expected
- /// string, got number; C# is already a
- /// string). Do not edit leftover ContextProxyTests,
+ /// Do not edit leftover ContextProxyTests,
/// ContextProxyAuthTests, or LoopbackHttpProxy.
///
[TestFixture]
@@ -167,6 +164,24 @@ public async Task ShouldWorkWhenPassingTheProxyOnlyOnTheContextLevel()
}
}
+ [PlaywrightTest("browsercontext-proxy.spec.ts", "should ignore legacy 'per-context' launch proxy")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public Task ShouldIgnoreLegacyPerContextLaunchProxy()
+ => ShouldIgnoreLegacyLaunchProxyAsync("per-context");
+
+ [PlaywrightTest("browsercontext-proxy.spec.ts", "should ignore legacy 'http://per-context' launch proxy")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public Task ShouldIgnoreLegacyHttpPerContextLaunchProxy()
+ => ShouldIgnoreLegacyLaunchProxyAsync("http://per-context");
+
+ [PlaywrightTest("browsercontext-proxy.spec.ts", "should throw for bad server value")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public void ShouldThrowForBadServerValue()
+ => Assert.Ignore("Proxy.Server is a string in C#, so a numeric server value cannot be passed.");
+
[PlaywrightTest("browsercontext-proxy.spec.ts", "should use proxy")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
@@ -768,6 +783,34 @@ private async Task ProxyLocalNetworkAsync(string target, bool additionalBypass)
private string[] NonFaviconUrls()
=> _proxy.RequestUrls.Where(url => url.IndexOf("favicon", StringComparison.Ordinal) < 0).ToArray();
+ private async Task ShouldIgnoreLegacyLaunchProxyAsync(string launchProxy)
+ {
+ EnsureServer();
+ _proxy.ForwardTo(ServerPort, allowConnectRequests: true);
+ IBrowser browser = await BrowserLauncher.LaunchAsync(proxy: new Proxy { Server = launchProxy }).ConfigureAwait(false);
+ try
+ {
+ IBrowserContext context = await browser.NewContextAsync(new() { Proxy = new Proxy { Server = _proxy.Host } }).ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.GoToAsync("http://non-existent.com/target.html").ConfigureAwait(false);
+ Assert.That(await page.TitleAsync().ConfigureAwait(false), Is.EqualTo("Served by the proxy"));
+ IAPIResponse response = await context.APIRequest.GetAsync("http://non-existent.com/target.html").ConfigureAwait(false);
+ Assert.That(await response.TextAsync().ConfigureAwait(false), Does.Contain("Served by the proxy"));
+ Assert.That(_proxy.ConnectHosts, Does.Contain("non-existent.com:80"));
+
+ IBrowserContext directContext = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage directPage = await directContext.NewPageAsync().ConfigureAwait(false);
+ await directPage.GoToAsync(Prefix + "/target.html").ConfigureAwait(false);
+ Assert.That(await directPage.TitleAsync().ConfigureAwait(false), Is.EqualTo("Served by the proxy"));
+ IAPIResponse directResponse = await directContext.APIRequest.GetAsync(Prefix + "/target.html").ConfigureAwait(false);
+ Assert.That(directResponse.Ok, Is.True);
+ }
+ finally
+ {
+ await DisposeQuietlyAsync(browser).ConfigureAwait(false);
+ }
+ }
+
private static async Task CatchGotoAsync(IPage page, string url)
{
try
diff --git a/src/PlaywrightNative.Tests/LibraryClientCertificatesParityTests.cs b/src/PlaywrightNative.Tests/LibraryClientCertificatesParityTests.cs
index ec4fbb1d..cf4f4a97 100644
--- a/src/PlaywrightNative.Tests/LibraryClientCertificatesParityTests.cs
+++ b/src/PlaywrightNative.Tests/LibraryClientCertificatesParityTests.cs
@@ -512,6 +512,41 @@ public async Task BrowserShouldPassWithMatchingCertificatesAndWhenAHttpProxyIsUs
}
}
+ [PlaywrightTest("client-certificates.spec.ts", "should respect launch proxy and proxy bypass")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task BrowserShouldRespectLaunchProxyAndProxyBypass()
+ {
+ await using OfficialClientCertificateServer server = await StartCcServerAsync()
+ .ConfigureAwait(false);
+ Uri serverUri = new Uri(server.Url);
+ await using OfficialTestProxy proxyServer = new OfficialTestProxy();
+ proxyServer.ForwardTo(serverUri.Port, allowConnectRequests: true);
+ string proxy = "localhost:" + proxyServer.Port.ToString(CultureInfo.InvariantCulture);
+ string serverHost = ProxiedConnectHost + ":" + serverUri.Port.ToString(CultureInfo.InvariantCulture);
+ ClientCertificate[] clientCertificates = new[] { Trusted(OriginOf(server.Url)) };
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync(proxy: new Proxy { Server = proxy }).ConfigureAwait(false);
+ {
+ IPage page = await browser.NewPageAsync(new() { IgnoreHTTPSErrors = true, ClientCertificates = clientCertificates })
+ .ConfigureAwait(false);
+ await page.GoToAsync(server.Url).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByTestId("message")).ToHaveTextAsync(TrustedMessage).ConfigureAwait(false);
+ // Headed Chromium also sends its own background requests through the launch proxy.
+ Assert.That(proxyServer.ConnectHosts, Does.Contain(serverHost));
+ await page.CloseAsync().ConfigureAwait(false);
+ }
+
+ proxyServer.ConnectHosts = Array.Empty();
+ {
+ IPage page = await browser.NewPageAsync(new() { IgnoreHTTPSErrors = true, ClientCertificates = clientCertificates, Proxy = new Proxy { Server = proxy, Bypass = serverUri.Host } })
+ .ConfigureAwait(false);
+ await page.GoToAsync(server.Url).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByTestId("message")).ToHaveTextAsync(TrustedMessage).ConfigureAwait(false);
+ Assert.That(proxyServer.ConnectHosts, Does.Not.Contain(serverHost));
+ await page.CloseAsync().ConfigureAwait(false);
+ }
+ }
+
[PlaywrightTest("client-certificates.spec.ts", "should pass with matching certificates and when a socks proxy is used")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
diff --git a/src/PlaywrightNative.Tests/LibraryFetchProxyParityTests.cs b/src/PlaywrightNative.Tests/LibraryFetchProxyParityTests.cs
index ea3b3f7b..5bd06c43 100644
--- a/src/PlaywrightNative.Tests/LibraryFetchProxyParityTests.cs
+++ b/src/PlaywrightNative.Tests/LibraryFetchProxyParityTests.cs
@@ -361,6 +361,56 @@ public async Task ShouldSendCorrectAlpnProtocolToHttpsProxy()
}
}
+ [PlaywrightTest("fetch-proxy.spec.ts", "should apply proxy.bypass to redirect targets")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldApplyProxyBypassToRedirectTargets()
+ {
+ EnsureServer();
+ string crossProcessHost = new Uri(CrossProcessPrefix).Authority;
+ if (crossProcessHost == new Uri(Prefix).Authority)
+ {
+ Assert.Ignore("Needs two different host names for the same server");
+ }
+
+ _proxy.ForwardTo(ServerPort, allowConnectRequests: true);
+ Server.SetRedirect("/redirect-to-cross-process", CrossProcessPrefix + "/simple.json");
+ Server.SetRedirect("/redirect-to-same-origin", Prefix + "/simple.json");
+ IBrowserContext context = await _browser.NewContextAsync(new()
+ {
+ Proxy = new Proxy
+ {
+ Server = "localhost:" + _proxy.Port.ToString(CultureInfo.InvariantCulture),
+ Bypass = new Uri(Prefix).Host,
+ }
+ }).ConfigureAwait(false);
+ try
+ {
+ {
+ // Bypassed first hop redirects to a host that must go through the proxy.
+ IAPIResponse response = await context.APIRequest.GetAsync(Prefix + "/redirect-to-cross-process").ConfigureAwait(false);
+ Assert.That(response.Url, Is.EqualTo(CrossProcessPrefix + "/simple.json"));
+ JsonElement? json = await response.JsonAsync().ConfigureAwait(false);
+ Assert.That(json.Value.GetProperty("foo").GetString(), Is.EqualTo("bar"));
+ Assert.That(_proxy.ConnectHosts, Is.EqualTo(new[] { crossProcessHost }));
+ _proxy.ConnectHosts = Array.Empty();
+ }
+
+ {
+ // Proxied first hop redirects to a bypassed host.
+ IAPIResponse response = await context.APIRequest.GetAsync(CrossProcessPrefix + "/redirect-to-same-origin").ConfigureAwait(false);
+ Assert.That(response.Url, Is.EqualTo(Prefix + "/simple.json"));
+ JsonElement? json = await response.JsonAsync().ConfigureAwait(false);
+ Assert.That(json.Value.GetProperty("foo").GetString(), Is.EqualTo("bar"));
+ Assert.That(_proxy.ConnectHosts, Is.EqualTo(new[] { crossProcessHost }));
+ }
+ }
+ finally
+ {
+ await context.CloseAsync().ConfigureAwait(false);
+ }
+ }
+
private static void EnsureServer()
{
if (Server == null)
diff --git a/src/PlaywrightNative.Tests/LibraryProxyParityTests.cs b/src/PlaywrightNative.Tests/LibraryProxyParityTests.cs
index 83086755..6f0c3b16 100644
--- a/src/PlaywrightNative.Tests/LibraryProxyParityTests.cs
+++ b/src/PlaywrightNative.Tests/LibraryProxyParityTests.cs
@@ -28,10 +28,7 @@ namespace PlaywrightNative.Tests
{
///
/// Official library/proxy.spec.ts parity. Launch-level
- /// browserType.launch({ proxy }). Skipped (Node-only type
- /// validation): should throw for bad server value
- /// (proxy.server: expected string, got number; C#
- /// is already a string). Official
+ /// browserType.launch({ proxy }). Official
/// it.fixme on should use proxy with emulated user agent.
/// Do not edit leftover ContextProxyTests,
/// ContextProxyAuthTests, or LoopbackHttpProxy.
@@ -130,6 +127,12 @@ public async Task TearDownAsync()
TestServerSetup.HttpsServer?.Reset();
}
+ [PlaywrightTest("proxy.spec.ts", "should throw for bad server value")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public void ShouldThrowForBadServerValue()
+ => Assert.Ignore("Proxy.Server is a string in C#, so a numeric server value cannot be passed.");
+
[PlaywrightTest("proxy.spec.ts", "should use proxy @smoke")]
[Test]
[Timeout(TestConstants.DefaultTestTimeout)]
diff --git a/src/PlaywrightNative/BrowserTypeLaunchOptions.cs b/src/PlaywrightNative/BrowserTypeLaunchOptions.cs
index 288b4915..f548d737 100644
--- a/src/PlaywrightNative/BrowserTypeLaunchOptions.cs
+++ b/src/PlaywrightNative/BrowserTypeLaunchOptions.cs
@@ -76,9 +76,9 @@ public class BrowserTypeLaunchOptions
public BrowserChannel Channel { get; set; }
///
- /// Network proxy used by the browser process. Chromium also needs a launch-level
- /// proxy (even a dummy per-context server) when contexts override
- /// with their own proxy.
+ /// Network proxy used by the browser process. Contexts can override it with their
+ /// own proxy in . The
+ /// legacy per-context placeholder server is ignored.
///
public Proxy Proxy { get; set; }
diff --git a/src/PlaywrightNative/Helpers/ProxySettings.cs b/src/PlaywrightNative/Helpers/ProxySettings.cs
index 110cdc06..5e871338 100644
--- a/src/PlaywrightNative/Helpers/ProxySettings.cs
+++ b/src/PlaywrightNative/Helpers/ProxySettings.cs
@@ -34,6 +34,16 @@ internal static class ProxySettings
internal static bool HasCredentials(Proxy proxy)
=> proxy != null && !string.IsNullOrEmpty(proxy.Username);
+ ///
+ /// Official browserType._validateLaunchOptions: drops the legacy
+ /// per-context placeholder launch proxy. It is no longer needed
+ /// for contexts to set their own proxy.
+ ///
+ /// Launch proxy, or .
+ /// The launch proxy, or for the placeholder.
+ internal static Proxy NormalizeLaunchProxy(Proxy proxy)
+ => proxy?.Server is "per-context" or "http://per-context" ? null : proxy;
+
///
/// Builds a proxy server URL. Adds an http:// scheme when missing.
/// When is and
diff --git a/src/PlaywrightNative/Playwright.cs b/src/PlaywrightNative/Playwright.cs
index 4ff0e5ba..304e3787 100644
--- a/src/PlaywrightNative/Playwright.cs
+++ b/src/PlaywrightNative/Playwright.cs
@@ -104,17 +104,18 @@ public static async Task LaunchChromiumAsync(BrowserTypeLaunchOptions
{
options ??= new BrowserTypeLaunchOptions();
BrowserTypeLaunchGuard.ThrowIfLaunchForbidden(options);
+ Proxy proxy = ProxySettings.NormalizeLaunchProxy(options.Proxy);
try
{
string executablePath = await ResolveExecutablePathAsync(SupportedBrowser.Chromium, options).ConfigureAwait(false);
PlaywrightNative.Chromium.CRBrowser crBrowser = await PlaywrightNative.Chromium.ChromiumBrowserType
- .LaunchAsync(executablePath, options.Headless, args: ToArgArray(options.Args), proxy: options.Proxy, chromiumSandbox: options.ChromiumSandbox, timeout: ResolveTimeout(options), ignoreDefaultArgs: options.IgnoreDefaultArgs, environment: options.Env, loggerFactory: options.LoggerFactory, devtools: options.Devtools, handleSIGINT: HandleSIGINT(options), handleSIGTERM: HandleSIGTERM(options), handleSIGHUP: HandleSIGHUP(options), ignoreDefaultArgsList: options.IgnoreDefaultArgsList)
+ .LaunchAsync(executablePath, options.Headless, args: ToArgArray(options.Args), proxy: proxy, chromiumSandbox: options.ChromiumSandbox, timeout: ResolveTimeout(options), ignoreDefaultArgs: options.IgnoreDefaultArgs, environment: options.Env, loggerFactory: options.LoggerFactory, devtools: options.Devtools, handleSIGINT: HandleSIGINT(options), handleSIGTERM: HandleSIGTERM(options), handleSIGHUP: HandleSIGHUP(options), ignoreDefaultArgsList: options.IgnoreDefaultArgsList)
.ConfigureAwait(false);
PlaywrightNative.Chromium.ChromiumBrowser chromium = new PlaywrightNative.Chromium.ChromiumBrowser(crBrowser, ResolveDownloadsPath(options), options.Logger)
{
- LaunchProxy = options.Proxy,
+ LaunchProxy = proxy,
};
((PlaywrightNative.Helpers.IHasTracesDir)chromium).TracesDir = options.TracesDir;
((PlaywrightNative.Helpers.IHasArtifactsDir)chromium).ArtifactsDir = options.ArtifactsDir;
@@ -170,16 +171,17 @@ public static async Task LaunchWebkitAsync(BrowserTypeLaunchOptions op
{
options ??= new BrowserTypeLaunchOptions();
BrowserTypeLaunchGuard.ThrowIfLaunchForbidden(options);
+ Proxy proxy = ProxySettings.NormalizeLaunchProxy(options.Proxy);
try
{
string executablePath = await ResolveExecutablePathAsync(SupportedBrowser.Webkit, options).ConfigureAwait(false);
WebKit.WKBrowser wkBrowser = await WebKit.WebkitBrowserType
- .LaunchAsync(executablePath, options.Headless, args: ToArgArray(options.Args), proxy: options.Proxy, timeout: ResolveTimeout(options), environment: options.Env, loggerFactory: options.LoggerFactory, handleSIGINT: HandleSIGINT(options), handleSIGTERM: HandleSIGTERM(options), handleSIGHUP: HandleSIGHUP(options))
+ .LaunchAsync(executablePath, options.Headless, args: ToArgArray(options.Args), proxy: proxy, timeout: ResolveTimeout(options), environment: options.Env, loggerFactory: options.LoggerFactory, handleSIGINT: HandleSIGINT(options), handleSIGTERM: HandleSIGTERM(options), handleSIGHUP: HandleSIGHUP(options))
.ConfigureAwait(false);
wkBrowser.LaunchDownloadsPath = ResolveDownloadsPath(options);
- wkBrowser.LaunchProxy = options.Proxy;
+ wkBrowser.LaunchProxy = proxy;
wkBrowser.Logger = options.Logger;
((PlaywrightNative.Helpers.IHasTracesDir)wkBrowser).TracesDir = options.TracesDir;
((PlaywrightNative.Helpers.IHasArtifactsDir)wkBrowser).ArtifactsDir = options.ArtifactsDir;
@@ -202,6 +204,7 @@ internal static async Task LaunchChromiumPersistentContextAsync
options ??= new BrowserTypeLaunchOptions();
BrowserTypeLaunchGuard.ThrowIfPersistentForbidden(options);
ThrowIfPageArgument(options.Args);
+ Proxy proxy = ProxySettings.NormalizeLaunchProxy(options.Proxy);
ClientCertificatesProxy certsProxy = null;
string resolvedDir = null;
try
@@ -209,7 +212,7 @@ internal static async Task LaunchChromiumPersistentContextAsync
string executablePath = await ResolveExecutablePathAsync(SupportedBrowser.Chromium, options).ConfigureAwait(false);
bool ownsUserDataDir = string.IsNullOrEmpty(userDataDir);
resolvedDir = ResolveUserDataDir(userDataDir);
- Proxy launchProxy = StartPersistentClientCertificates(options, out certsProxy);
+ Proxy launchProxy = StartPersistentClientCertificates(options, proxy, out certsProxy);
PlaywrightNative.Chromium.CRBrowser crBrowser = await PlaywrightNative.Chromium.ChromiumBrowserType
.LaunchAsync(
@@ -234,14 +237,14 @@ internal static async Task LaunchChromiumPersistentContextAsync
PlaywrightNative.Chromium.ChromiumBrowser instance = new(crBrowser, ResolveDownloadsPath(options))
{
- LaunchProxy = options.Proxy,
+ LaunchProxy = proxy,
};
((PlaywrightNative.Helpers.IHasTracesDir)instance).TracesDir = options.TracesDir;
((PlaywrightNative.Helpers.IHasArtifactsDir)instance).ArtifactsDir = options.ArtifactsDir;
IBrowserContext context = instance.PersistentContext();
if (context is PlaywrightNative.Chromium.ChromiumBrowserContext chromiumCerts)
{
- chromiumCerts.AttachClientCertificatesProxy(certsProxy, options.Proxy);
+ chromiumCerts.AttachClientCertificatesProxy(certsProxy, proxy);
certsProxy = null;
}
@@ -315,13 +318,14 @@ internal static async Task LaunchWebkitPersistentContextAsync(s
options ??= new BrowserTypeLaunchOptions();
BrowserTypeLaunchGuard.ThrowIfPersistentForbidden(options);
ThrowIfPageArgument(options.Args);
+ Proxy proxy = ProxySettings.NormalizeLaunchProxy(options.Proxy);
ClientCertificatesProxy webkitCertsProxy = null;
try
{
string executablePath = await ResolveExecutablePathAsync(SupportedBrowser.Webkit, options).ConfigureAwait(false);
bool ownsUserDataDir = string.IsNullOrEmpty(userDataDir);
string resolvedDir = ResolveUserDataDir(userDataDir);
- Proxy webkitLaunchProxy = StartPersistentClientCertificates(options, out webkitCertsProxy);
+ Proxy webkitLaunchProxy = StartPersistentClientCertificates(options, proxy, out webkitCertsProxy);
WebKit.WKBrowser wkBrowser = await WebKit.WebkitBrowserType
.LaunchAsync(
@@ -341,13 +345,13 @@ internal static async Task LaunchWebkitPersistentContextAsync(s
.ConfigureAwait(false);
wkBrowser.LaunchDownloadsPath = ResolveDownloadsPath(options);
- wkBrowser.LaunchProxy = options.Proxy;
+ wkBrowser.LaunchProxy = proxy;
((PlaywrightNative.Helpers.IHasTracesDir)wkBrowser).TracesDir = options.TracesDir;
((PlaywrightNative.Helpers.IHasArtifactsDir)wkBrowser).ArtifactsDir = options.ArtifactsDir;
IBrowserContext context = wkBrowser.PersistentContext();
if (context is WebKit.WKBrowserContext webkitCerts)
{
- webkitCerts.AttachClientCertificatesProxy(webkitCertsProxy, options.Proxy);
+ webkitCerts.AttachClientCertificatesProxy(webkitCertsProxy, proxy);
webkitCertsProxy = null;
}
@@ -396,19 +400,20 @@ await PatchWebKitNavigatorLanguageAsync(webkitPatch, persistentPatch.Locale)
private static Proxy StartPersistentClientCertificates(
BrowserTypeLaunchOptions options,
+ Proxy launchProxy,
out ClientCertificatesProxy proxy)
{
proxy = null;
if (options is not BrowserTypeLaunchPersistentContextOptions persistent
|| !ClientCertificateHelper.HasAny(persistent.ClientCertificates))
{
- return options?.Proxy;
+ return launchProxy;
}
proxy = ClientCertificatesProxy.Create(
persistent.ClientCertificates,
persistent.IgnoreHTTPSErrors == true,
- options.Proxy);
+ launchProxy);
return proxy.BrowserProxy;
}