Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,13 @@ protected async Task RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedS

await cws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);

// There is a race condition in the above. If the ReceiveAsync receives the sent close message from the server,
// then it will complete successfully and the socket will close successfully. If the CloseAsync receive the sent
// close message from the server, then the receive async will end up getting aborted along with the socket.
// The outcome depends on timing. Two outcomes are acceptable:
// 1. The pending ReceiveAsync picks up the server's close frame cleanly: it completes with a Close
// message and the socket transitions to Closed.
// 2. The close handshake triggers an internal Abort (e.g. WaitForServerToCloseConnectionAsync
// times out because the server doesn't close TCP within its 1s window): state becomes Aborted,
// the parked receive's stream read fails, and ReceiveAsyncPrivate translates the exception to
// OperationCanceledException because of the Aborted state.
try
{
await t;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ internal sealed partial class ManagedWebSocket : WebSocket
private bool _sentCloseFrame;
/// <summary>Whether we've ever received a close frame.</summary>
private bool _receivedCloseFrame;
/// <summary>0 if <see cref="WaitForServerToCloseConnectionAsync"/> has not been invoked yet; 1 otherwise.</summary>
private int _waitedForServerClose;
/// <summary>The reason for the close, as sent by the server, or null if not yet closed.</summary>
private WebSocketCloseStatus? _closeStatus;
/// <summary>A description of the close reason as sent by the server, or null if not yet closed.</summary>
Expand Down Expand Up @@ -1123,9 +1125,18 @@ private async ValueTask HandleReceivedCloseAsync(MessageHeader header, Cancellat
}
}

/// <summary>Issues a read on the stream to wait for EOF.</summary>
/// <summary>Issues a read on the stream to wait for EOF. Idempotent across call sites.</summary>
private async ValueTask WaitForServerToCloseConnectionAsync(CancellationToken cancellationToken)
{
// Called from both the receive and the send path. In rare concurrent Close + Receive
// scenarios both paths can observe the close handshake as complete and would otherwise
// both issue a stream read (violating most Stream implementations' single-consumer invariant).
// The flag guarantees that only the first invocation performs the wait; the other is a no-op.
if (Interlocked.CompareExchange(ref _waitedForServerClose, 1, 0) != 0)
{
return;
}

if (NetEventSource.Log.IsEnabled()) NetEventSource.Trace(this);

// Per RFC 6455 7.1.1, try to let the server close the connection. We give it up to a second.
Expand Down
Loading