Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 1824535

Browse files
authored
Fix ClientWebSocket closing handshake logic (#36975)
An HttpListener websocket test was failing after the change from PR #36928. That PR made changes to tighten up the transition to the Closed state after the closing handshake had completed. That PR missed some additional changes needed especially in cases where a server (such as loopback) sends the close frame almost concurrently with the client sending the close frame. This PR fixes the close frame handshake logic and also makes some optimizations in cases where the websocket has already transitioned to the Aborted state during the closing handshake. In that case, the websocket should not wait for a close frame to be received but should simply proceed to closing the connection. Fixes #36963
1 parent 65f07a9 commit 1824535

File tree

2 files changed

+35
-30
lines changed

2 files changed

+35
-30
lines changed

src/Common/src/System/Net/WebSockets/ManagedWebSocket.cs

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,50 +1063,56 @@ private async Task CloseAsyncPrivate(WebSocketCloseStatus closeStatus, string st
10631063
await SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken).ConfigureAwait(false);
10641064
}
10651065

1066-
// We should now either be in a CloseSent case (because we just sent one), or in a CloseReceived state, in case
1066+
// We should now either be in a CloseSent case (because we just sent one), or in a Closed state, in case
10671067
// there was a concurrent receive that ended up handling an immediate close frame response from the server.
10681068
// Of course it could also be Aborted if something happened concurrently to cause things to blow up.
10691069
Debug.Assert(
10701070
State == WebSocketState.CloseSent ||
1071-
State == WebSocketState.CloseReceived ||
1071+
State == WebSocketState.Closed ||
10721072
State == WebSocketState.Aborted,
10731073
$"Unexpected state {State}.");
10741074

1075-
// Wait until we've received a close response
1076-
byte[] closeBuffer = ArrayPool<byte>.Shared.Rent(MaxMessageHeaderLength + MaxControlPayloadLength);
1077-
try
1075+
// We only need to wait for a received close frame if we are in the CloseSent State. If we are in the Closed
1076+
// State then it means we already received a close frame. If we are in the Aborted State, then we should not
1077+
// wait for a close frame as per RFC 6455 Section 7.1.7 "Fail the WebSocket Connection".
1078+
if (State == WebSocketState.CloseSent)
10781079
{
1079-
while (!_receivedCloseFrame)
1080+
// Wait until we've received a close response
1081+
byte[] closeBuffer = ArrayPool<byte>.Shared.Rent(MaxMessageHeaderLength + MaxControlPayloadLength);
1082+
try
10801083
{
1081-
Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}");
1082-
Task receiveTask;
1083-
lock (ReceiveAsyncLock)
1084+
while (!_receivedCloseFrame)
10841085
{
1085-
// Now that we're holding the ReceiveAsyncLock, double-check that we've not yet received the close frame.
1086-
// It could have been received between our check above and now due to a concurrent receive completing.
1087-
if (_receivedCloseFrame)
1086+
Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}");
1087+
Task receiveTask;
1088+
lock (ReceiveAsyncLock)
10881089
{
1089-
break;
1090+
// Now that we're holding the ReceiveAsyncLock, double-check that we've not yet received the close frame.
1091+
// It could have been received between our check above and now due to a concurrent receive completing.
1092+
if (_receivedCloseFrame)
1093+
{
1094+
break;
1095+
}
1096+
1097+
// We've not yet processed a received close frame, which means we need to wait for a received close to complete.
1098+
// There may already be one in flight, in which case we want to just wait for that one rather than kicking off
1099+
// another (we don't support concurrent receive operations). We need to kick off a new receive if either we've
1100+
// never issued a receive or if the last issued receive completed for reasons other than a close frame. There is
1101+
// a race condition here, e.g. if there's a in-flight receive that completes after we check, but that's fine: worst
1102+
// case is we then await it, find that it's not what we need, and try again.
1103+
receiveTask = _lastReceiveAsync;
1104+
_lastReceiveAsync = receiveTask = ValidateAndReceiveAsync(receiveTask, closeBuffer, cancellationToken);
10901105
}
10911106

1092-
// We've not yet processed a received close frame, which means we need to wait for a received close to complete.
1093-
// There may already be one in flight, in which case we want to just wait for that one rather than kicking off
1094-
// another (we don't support concurrent receive operations). We need to kick off a new receive if either we've
1095-
// never issued a receive or if the last issued receive completed for reasons other than a close frame. There is
1096-
// a race condition here, e.g. if there's a in-flight receive that completes after we check, but that's fine: worst
1097-
// case is we then await it, find that it's not what we need, and try again.
1098-
receiveTask = _lastReceiveAsync;
1099-
_lastReceiveAsync = receiveTask = ValidateAndReceiveAsync(receiveTask, closeBuffer, cancellationToken);
1107+
// Wait for whatever receive task we have. We'll then loop around again to re-check our state.
1108+
Debug.Assert(receiveTask != null);
1109+
await receiveTask.ConfigureAwait(false);
11001110
}
1101-
1102-
// Wait for whatever receive task we have. We'll then loop around again to re-check our state.
1103-
Debug.Assert(receiveTask != null);
1104-
await receiveTask.ConfigureAwait(false);
11051111
}
1106-
}
1107-
finally
1108-
{
1109-
ArrayPool<byte>.Shared.Return(closeBuffer);
1112+
finally
1113+
{
1114+
ArrayPool<byte>.Shared.Return(closeBuffer);
1115+
}
11101116
}
11111117

11121118
// We're closed. Close the connection and update the status.

src/System.Net.HttpListener/tests/HttpListenerWebSocketTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ public static IEnumerable<object[]> CloseStatus_Valid_TestData()
172172
yield return new object[] { WebSocketCloseStatus.MandatoryExtension, "StatusDescription", WebSocketCloseStatus.MandatoryExtension };
173173
}
174174

175-
[ActiveIssue(36963)]
176175
[ConditionalTheory(nameof(IsNotWindows7AndIsWindowsImplementation))] // [ActiveIssue(20396, TestPlatforms.AnyUnix)]
177176
[MemberData(nameof(CloseStatus_Valid_TestData))]
178177
public async Task CloseOutputAsync_HandshakeStartedFromClient_Success(WebSocketCloseStatus status, string statusDescription, WebSocketCloseStatus expectedCloseStatus)

0 commit comments

Comments
 (0)