Skip to content

[Bug]: WebSocketRoute crashes with KeyNotFoundException when a close event omits code/reason during Browser.CloseAsync with a connected mock route #3333

Description

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]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions