diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs index b767c3149bee43..25cecaf3e3d9d6 100644 --- a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs +++ b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs @@ -123,6 +123,7 @@ internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellati throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted); } + CancellationTokenRegistration connectRegistration = cancellationToken.Register(cts => ((CancellationTokenSource)cts!).Cancel(), _cts); TaskCompletionSource tcsConnect = new TaskCompletionSource(); // For Abort/Dispose. Calling Abort on the request at any point will close the connection. @@ -164,7 +165,14 @@ internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellati NativeCleanup(); if ((InternalState)_state == InternalState.Connecting) { - tcsConnect.TrySetException(new WebSocketException(WebSocketError.NativeError)); + if (cancellationToken.IsCancellationRequested) + { + tcsConnect.TrySetCanceled(cancellationToken); + } + else + { + tcsConnect.TrySetException(new WebSocketException(WebSocketError.NativeError)); + } } else { @@ -214,8 +222,17 @@ internal async Task ConnectAsyncJavaScript(Uri uri, CancellationToken cancellati catch (Exception wse) { Dispose(); - WebSocketException wex = new WebSocketException(SR.net_webstatus_ConnectFailure, wse); - throw wex; + switch (wse) + { + case OperationCanceledException: + throw; + default: + throw new WebSocketException(SR.net_webstatus_ConnectFailure, wse); + } + } + finally + { + connectRegistration.Unregister(); } } @@ -324,7 +341,7 @@ public override void Dispose() // and called by Dispose or Abort so that any open websocket connection can be closed. private async void AbortRequest() { - if (State == WebSocketState.Open) + if (State == WebSocketState.Open || State == WebSocketState.Connecting) { await CloseAsyncCore(WebSocketCloseStatus.NormalClosure, SR.net_WebSockets_Connection_Aborted, CancellationToken.None).ConfigureAwait(continueOnCapturedContext: true); } @@ -455,7 +472,7 @@ public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string? private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, CancellationToken cancellationToken) { - ThrowOnInvalidState(State, WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent); + ThrowOnInvalidState(State, WebSocketState.Connecting, WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent); WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription); diff --git a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Browser.cs b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Browser.cs index 97bdfdf28d84a0..cf91e6821db05e 100644 --- a/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Browser.cs +++ b/src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Browser.cs @@ -31,6 +31,7 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli { try { + cancellationToken.ThrowIfCancellationRequested(); // avoid allocating a WebSocket object if cancellation was requested before connect CancellationTokenSource? linkedCancellation; CancellationTokenSource externalAndAbortCancellation; if (cancellationToken.CanBeCanceled) // avoid allocating linked source if external token is not cancelable @@ -61,11 +62,13 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli Abort(); - if (exc is WebSocketException) - { - throw; + switch (exc) { + case WebSocketException: + case OperationCanceledException _ when cancellationToken.IsCancellationRequested: + throw; + default: + throw new WebSocketException(SR.net_webstatus_ConnectFailure, exc); } - throw new WebSocketException(SR.net_webstatus_ConnectFailure, exc); } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 45c94e41644280..24bfbc26c44aa1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -248,7 +248,6 @@ public async Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri se } [ConditionalFact(nameof(WebSocketsSupported))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/44720", TestPlatforms.Browser)] public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperationCanceledException() { using (var clientSocket = new ClientWebSocket()) @@ -260,6 +259,18 @@ public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperatio } } + [ConditionalFact(nameof(WebSocketsSupported))] + public async Task ConnectAsync_CancellationRequestedInflightConnect_ThrowsOperationCanceledException() + { + using (var clientSocket = new ClientWebSocket()) + { + var cts = new CancellationTokenSource(); + Task t = clientSocket.ConnectAsync(new Uri("ws://" + Guid.NewGuid().ToString("N")), cts.Token); + cts.Cancel(); + await Assert.ThrowsAnyAsync(() => t); + } + } + [ConditionalFact(nameof(WebSocketsSupported))] [ActiveIssue("https://github.com/dotnet/runtime/issues/34690", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] [ActiveIssue("https://github.com/dotnet/runtime/issues/42852", TestPlatforms.Browser)] diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index 110f737ed6e85d..1f0ccd55913214 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -124,6 +125,10 @@ public static async Task Retry(ITestOutputHelper output, Func> fun private static bool InitWebSocketSupported() { ClientWebSocket cws = null; + if (PlatformDetection.IsBrowser && !PlatformDetection.IsBrowserDomSupported) + { + return false; + } try {