Version
1.61.0
Steps to reproduce
using Microsoft.Playwright;
using var pw = await Playwright.CreateAsync();
var browser = await pw.Chromium.LaunchAsync(new() { Headless = true });
var context = await browser.NewContextAsync();
await context.RouteAsync("**/*", route => route.FulfillAsync(new()
{
Status = 200,
ContentType = "text/html",
Body = "<script>new WebSocket('wss://mock.local/ws');</script>",
}));
// mock server: never calls ConnectToServerAsync
await context.RouteWebSocketAsync("**/ws", ws => ws.OnMessage(frame => ws.Send(frame.Text)));
var page = await context.NewPageAsync();
await page.GotoAsync("https://mock.local/");
await page.WaitForTimeoutAsync(500); // let the WS connect
await browser.CloseAsync(); // <- throws
Expected behavior
Browser.CloseAsync() should not throw in this case.
Actual behavior
Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
at System.Text.Json.JsonElement.GetProperty(String propertyName)
at Microsoft.Playwright.Core.WebSocketRoute.OnMessage(String method, JsonElement serverParams) in /_/src/Playwright/Core/WebSocketRoute.cs:line 99
at Microsoft.Playwright.Transport.Connection.Dispatch(PlaywrightServerMessage message)
...
at Microsoft.Playwright.Core.Browser.CloseAsync(BrowserCloseOptions options)
Additional context
Analysis
In the wire protocol, the closePage / closeServer events of WebSocketRoute carry optional
code and reason — the TypeScript client types the handlers accordingly
(packages/playwright-core/src/client/network.ts):
private _onServerClose?: (code: number | undefined, reason: string | undefined) => any;
But WebSocketRoute.OnMessage in the .NET binding reads them with the throwing accessor for both
events, in both the handler branch and the forwarding branch :
serverParams.GetProperty("code").GetInt32()
serverParams.GetProperty("reason").GetString()
serverParams.GetProperty("wasClean").GetBoolean()
When the browser (or page/context) is closed while a mocked WebSocket is still connected the close
event arrives without code/reason, JsonElement.GetProperty throws and the exception escapes
through Connection.Dispatch, failing the in-flight Browser.CloseAsync() call.
It should probably use TryGetProperty with null/default value fallback instead ?
Workaround
For now, I track live IWebSocketRoute instances and close them explicitly with a code before tearing down
pages/context/browser:
await ws.CloseAsync(new() { Code = 1000, Reason = "teardown" });
Environment
- Operating System: [Windows 11]
- CPU: [x64]
- Browser: [Chromium]
- .NET Version (TFM): [net10.0]
Version
1.61.0
Steps to reproduce
Expected behavior
Browser.CloseAsync()should not throw in this case.Actual behavior
Additional context
Analysis
In the wire protocol, the
closePage/closeServerevents ofWebSocketRoutecarry optionalcodeandreason— the TypeScript client types the handlers accordingly(
packages/playwright-core/src/client/network.ts):But
WebSocketRoute.OnMessagein the .NET binding reads them with the throwing accessor for bothevents, in both the handler branch and the forwarding branch :
When the browser (or page/context) is closed while a mocked WebSocket is still connected the close
event arrives without
code/reason,JsonElement.GetPropertythrows and the exception escapesthrough
Connection.Dispatch, failing the in-flightBrowser.CloseAsync()call.It should probably use
TryGetPropertywith null/default value fallback instead ?Workaround
For now, I track live
IWebSocketRouteinstances and close them explicitly with a code before tearing downpages/context/browser:
Environment