From 7917c958e23d17733abf894c4357d03aed0e584b Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:34:54 +0300 Subject: [PATCH 1/6] [dotnet] [bidi] Properly handle websocket close handshake --- dotnet/src/webdriver/BiDi/WebSocketTransport.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs index 5f9ba5333e8e1..245d01a4439cd 100644 --- a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs +++ b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs @@ -67,6 +67,14 @@ public async Task ReceiveAsync(CancellationToken cancellationToken) { result = await _webSocket.ReceiveAsync(segment, cancellationToken).ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) + { + await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); + + throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely, + $"The remote end closed the WebSocket connection. Status: {_webSocket.CloseStatus}, Description: {_webSocket.CloseStatusDescription}"); + } + _sharedMemoryStream.Write(receiveBuffer, 0, result.Count); } while (!result.EndOfMessage); From 1d18a0ceac61378631dda07f96c6319687e2da58 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:38:04 +0300 Subject: [PATCH 2/6] Fail all pending commands --- dotnet/src/webdriver/BiDi/Broker.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 8f01585ec04d9..19d66e703594e 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -269,6 +269,14 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) _logger.Error($"Unhandled error occurred while receiving remote messages: {ex}"); } + // Fail all pending commands, as the connection is likely broken if we failed to receive messages. + foreach (var pendingCommand in _pendingCommands.Values) + { + pendingCommand.TaskCompletionSource.TrySetException(ex); + } + + _pendingCommands.Clear(); + throw; } } From 9605a41681bd57a5a6e4012189df4286e951cd03 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:42:39 +0300 Subject: [PATCH 3/6] Dispose via close handshake --- dotnet/src/webdriver/BiDi/Broker.cs | 2 +- dotnet/src/webdriver/BiDi/ITransport.cs | 2 +- .../src/webdriver/BiDi/WebSocketTransport.cs | 35 +++++++++++-------- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 19d66e703594e..7e0805e679f22 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -113,7 +113,7 @@ public async ValueTask DisposeAsync() _receiveMessagesCancellationTokenSource.Dispose(); - _transport.Dispose(); + await _transport.DisposeAsync().ConfigureAwait(false); GC.SuppressFinalize(this); } diff --git a/dotnet/src/webdriver/BiDi/ITransport.cs b/dotnet/src/webdriver/BiDi/ITransport.cs index bdf33406b3936..f202535253c7b 100644 --- a/dotnet/src/webdriver/BiDi/ITransport.cs +++ b/dotnet/src/webdriver/BiDi/ITransport.cs @@ -19,7 +19,7 @@ namespace OpenQA.Selenium.BiDi; -interface ITransport : IDisposable +interface ITransport : IAsyncDisposable { Task ReceiveAsync(CancellationToken cancellationToken); diff --git a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs index 245d01a4439cd..84c60cb2954da 100644 --- a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs +++ b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi; -sealed class WebSocketTransport(ClientWebSocket webSocket) : ITransport, IDisposable +sealed class WebSocketTransport(ClientWebSocket webSocket) : ITransport { private readonly static ILogger _logger = Internal.Logging.Log.GetLogger(); @@ -115,26 +115,31 @@ public async Task SendAsync(byte[] data, CancellationToken cancellationToken) private bool _disposed; - public void Dispose() + public async ValueTask DisposeAsync() { - Dispose(true); - GC.SuppressFinalize(this); - } + if (_disposed) return; - private void Dispose(bool disposing) - { - if (_disposed) + if (_webSocket.State == WebSocketState.Open) { - return; + try + { + await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogEventLevel.Warn)) + { + _logger.Warn($"Error closing WebSocket gracefully: {ex.Message}"); + } + } } - if (disposing) - { - _webSocket.Dispose(); - _sharedMemoryStream.Dispose(); - _socketSendSemaphoreSlim.Dispose(); - } + _webSocket.Dispose(); + _sharedMemoryStream.Dispose(); + _socketSendSemaphoreSlim.Dispose(); _disposed = true; + + GC.SuppressFinalize(this); } } From 2c30a5de189af138fa084fd61dde114dda514ec6 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:58:48 +0300 Subject: [PATCH 4/6] Cancellation free CloseOutputAsync --- dotnet/src/webdriver/BiDi/WebSocketTransport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs index 84c60cb2954da..33d57fbe7b18b 100644 --- a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs +++ b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs @@ -69,7 +69,7 @@ public async Task ReceiveAsync(CancellationToken cancellationToken) if (result.MessageType == WebSocketMessageType.Close) { - await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken).ConfigureAwait(false); + await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely, $"The remote end closed the WebSocket connection. Status: {_webSocket.CloseStatus}, Description: {_webSocket.CloseStatusDescription}"); From 27f6851d3f11539ab51b802ea84424b674af63d6 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:00:58 +0300 Subject: [PATCH 5/6] Accurate error message --- dotnet/src/webdriver/BiDi/WebSocketTransport.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs index 33d57fbe7b18b..fcab07b9a7770 100644 --- a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs +++ b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs @@ -72,7 +72,7 @@ public async Task ReceiveAsync(CancellationToken cancellationToken) await _webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false); throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely, - $"The remote end closed the WebSocket connection. Status: {_webSocket.CloseStatus}, Description: {_webSocket.CloseStatusDescription}"); + $"The remote end closed the WebSocket connection. Status: {result.CloseStatus}, Description: {result.CloseStatusDescription}"); } _sharedMemoryStream.Write(receiveBuffer, 0, result.Count); From 95af74b15bfe0f829a87053eb391223190bc56ed Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 20:10:12 +0300 Subject: [PATCH 6/6] Iterate pending commands --- dotnet/src/webdriver/BiDi/Broker.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 7e0805e679f22..162fc392fa920 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -270,13 +270,14 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) } // Fail all pending commands, as the connection is likely broken if we failed to receive messages. - foreach (var pendingCommand in _pendingCommands.Values) + foreach (var id in _pendingCommands.Keys) { - pendingCommand.TaskCompletionSource.TrySetException(ex); + if (_pendingCommands.TryRemove(id, out var pendingCommand)) + { + pendingCommand.TaskCompletionSource.TrySetException(ex); + } } - _pendingCommands.Clear(); - throw; } }