From 20cadac3a543808b8da361d7e9dc33b0c147f2f5 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 12 Aug 2026 15:20:51 +0100 Subject: [PATCH] fix(webSocketRoute): handle close events without code/reason The closePage/closeServer events carry optional code and reason, but they were read with throwing accessors. When a page with a live routed WebSocket navigated or closed, the missing properties threw KeyNotFoundException inside Connection.Dispatch, closing the entire connection. Fixes: https://github.com/microsoft/playwright-dotnet/issues/3333 --- .../PageRouteWebSocketTests.cs | 45 ++++++++++++++++ src/Playwright/Core/WebSocketRoute.cs | 52 +++++++++++-------- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/src/Playwright.Tests/PageRouteWebSocketTests.cs b/src/Playwright.Tests/PageRouteWebSocketTests.cs index 0dd3b9946..7f0bce4ba 100644 --- a/src/Playwright.Tests/PageRouteWebSocketTests.cs +++ b/src/Playwright.Tests/PageRouteWebSocketTests.cs @@ -287,6 +287,51 @@ await AssertAreEqualWithRetriesAsync(() => Page.EvaluateAsync("() => w ]); } + [PlaywrightTest("route-web-socket.spec.ts", "should emit close upon frame navigation")] + public async Task ShouldEmitCloseUponFrameNavigation() + { + var tcs = new TaskCompletionSource(); + await Page.RouteWebSocketAsync(new Regex(".*"), ws => + { + ws.ConnectToServer(); + tcs.SetResult(ws); + }); + + await SetupWS(Page, Server.Port, "blob"); + + var route = await tcs.Task; + route.Send("hello"); + + await AssertAreEqualWithRetriesAsync(() => Page.EvaluateAsync("() => window.log"), new[] + { + "open", + $"message: data=hello origin=ws://localhost:{Server.Port} lastEventId=", + }); + + var closedTcs = new TaskCompletionSource(); + route.OnClose((code, reason) => closedTcs.TrySetResult()); + await Page.GotoAsync(Server.EmptyPage); + await closedTcs.Task; + } + + [PlaywrightTest("route-web-socket.spec.ts", "should not throw after page closure")] + public async Task ShouldNotThrowAfterPageClosure() + { + var tcs = new TaskCompletionSource(); + await Page.RouteWebSocketAsync(new Regex(".*"), ws => + { + ws.ConnectToServer(); + tcs.SetResult(ws); + }); + + await SetupWS(Page, Server.Port, "blob"); + + var route = await tcs.Task; + var closeTask = Page.CloseAsync(); + route.Send("hello"); + await closeTask; + } + [PlaywrightTest("page-route-web-socket.spec.ts", "should work with baseURL")] public async Task ShouldWorkWithBaseURL() { diff --git a/src/Playwright/Core/WebSocketRoute.cs b/src/Playwright/Core/WebSocketRoute.cs index 6896b299d..8532f5264 100644 --- a/src/Playwright/Core/WebSocketRoute.cs +++ b/src/Playwright/Core/WebSocketRoute.cs @@ -90,35 +90,43 @@ internal override void OnMessage(string method, JsonElement serverParams) } break; case "closePage": - if (_onPageClose != null) { - _onPageClose(serverParams.GetProperty("code").GetInt32(), serverParams.GetProperty("reason").GetString()); - } - else - { - SendMessageToServerAsync("closeServer", new Dictionary + int? code = serverParams.TryGetProperty("code", out var codeElement) && codeElement.ValueKind == JsonValueKind.Number ? codeElement.GetInt32() : null; + string? reason = serverParams.TryGetProperty("reason", out var reasonElement) && reasonElement.ValueKind == JsonValueKind.String ? reasonElement.GetString() : null; + if (_onPageClose != null) { - ["code"] = serverParams.GetProperty("code").GetInt32(), - ["reason"] = serverParams.GetProperty("reason").GetString(), - ["wasClean"] = serverParams.GetProperty("wasClean").GetBoolean(), - }).IgnoreException(); + _onPageClose(code, reason); + } + else + { + SendMessageToServerAsync("closeServer", new Dictionary + { + ["code"] = code, + ["reason"] = reason, + ["wasClean"] = serverParams.GetProperty("wasClean").GetBoolean(), + }).IgnoreException(); + } + break; } - break; case "closeServer": - if (_onServerClose != null) { - _onServerClose(serverParams.GetProperty("code").GetInt32(), serverParams.GetProperty("reason").GetString()); - } - else - { - SendMessageToServerAsync("closePage", new Dictionary + int? code = serverParams.TryGetProperty("code", out var codeElement) && codeElement.ValueKind == JsonValueKind.Number ? codeElement.GetInt32() : null; + string? reason = serverParams.TryGetProperty("reason", out var reasonElement) && reasonElement.ValueKind == JsonValueKind.String ? reasonElement.GetString() : null; + if (_onServerClose != null) { - ["code"] = serverParams.GetProperty("code").GetInt32(), - ["reason"] = serverParams.GetProperty("reason").GetString(), - ["wasClean"] = serverParams.GetProperty("wasClean").GetBoolean(), - }).IgnoreException(); + _onServerClose(code, reason); + } + else + { + SendMessageToServerAsync("closePage", new Dictionary + { + ["code"] = code, + ["reason"] = reason, + ["wasClean"] = serverParams.GetProperty("wasClean").GetBoolean(), + }).IgnoreException(); + } + break; } - break; } }