Skip to content

Commit 5e9e29f

Browse files
authored
[release/8.0][browser] BrowserWebSocket.ReceiveAsync after server initiated close (#97002)
1 parent e24179e commit 5e9e29f

6 files changed

Lines changed: 205 additions & 61 deletions

File tree

src/libraries/Common/src/System/Net/WebSockets/WebSocketValidate.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ internal static partial class WebSocketValidate
2323
internal const int MaxDeflateWindowBits = 15;
2424

2525
internal const int MaxControlFramePayloadLength = 123;
26+
#if TARGET_BROWSER
27+
private const int ValidCloseStatusCodesFrom = 3000;
28+
private const int ValidCloseStatusCodesTo = 4999;
29+
#else
2630
private const int CloseStatusCodeAbort = 1006;
2731
private const int CloseStatusCodeFailedTLSHandshake = 1015;
2832
private const int InvalidCloseStatusCodesFrom = 0;
2933
private const int InvalidCloseStatusCodesTo = 999;
34+
#endif
3035

3136
// [0x21, 0x7E] except separators "()<>@,;:\\\"/[]?={} ".
3237
private static readonly SearchValues<char> s_validSubprotocolChars =
@@ -84,11 +89,15 @@ internal static void ValidateCloseStatus(WebSocketCloseStatus closeStatus, strin
8489
}
8590

8691
int closeStatusCode = (int)closeStatus;
87-
92+
#if TARGET_BROWSER
93+
// as defined in https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#code
94+
if (closeStatus != WebSocketCloseStatus.NormalClosure && (closeStatusCode < ValidCloseStatusCodesFrom || closeStatusCode > ValidCloseStatusCodesTo))
95+
#else
8896
if ((closeStatusCode >= InvalidCloseStatusCodesFrom &&
8997
closeStatusCode <= InvalidCloseStatusCodesTo) ||
9098
closeStatusCode == CloseStatusCodeAbort ||
9199
closeStatusCode == CloseStatusCodeFailedTLSHandshake)
100+
#endif
92101
{
93102
// CloseStatus 1006 means Aborted - this will never appear on the wire and is reflected by calling WebSocket.Abort
94103
throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCloseStatusCode,

src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ public static async Task InvokeAsync(HttpContext context)
2424

2525
if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay10sec"))
2626
{
27-
Thread.Sleep(10000);
27+
await Task.Delay(10000);
2828
}
2929
else if (context.Request.QueryString.HasValue && context.Request.QueryString.Value.Contains("delay20sec"))
3030
{
31-
Thread.Sleep(20000);
31+
await Task.Delay(20000);
3232
}
3333

3434
try
@@ -124,14 +124,15 @@ await socket.CloseAsync(
124124
}
125125

126126
bool sendMessage = false;
127+
string receivedMessage = null;
127128
if (receiveResult.MessageType == WebSocketMessageType.Text)
128129
{
129-
string receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset);
130+
receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset);
130131
if (receivedMessage == ".close")
131132
{
132133
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
133134
}
134-
if (receivedMessage == ".shutdown")
135+
else if (receivedMessage == ".shutdown")
135136
{
136137
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
137138
}
@@ -161,6 +162,14 @@ await socket.SendAsync(
161162
!replyWithPartialMessages,
162163
CancellationToken.None);
163164
}
165+
if (receivedMessage == ".closeafter")
166+
{
167+
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
168+
}
169+
else if (receivedMessage == ".shutdownafter")
170+
{
171+
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None);
172+
}
164173
}
165174
}
166175
}

src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/BrowserWebSockets/BrowserWebSocket.cs

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ internal sealed class BrowserWebSocket : WebSocket
2424
private WebSocketState _state;
2525
private bool _disposed;
2626
private bool _aborted;
27+
private bool _closeReceived;
28+
private bool _closeSent;
2729
private int[] responseStatus = new int[3];
2830
private MemoryHandle? responseStatusHandle;
2931

@@ -37,7 +39,7 @@ public override WebSocketState State
3739
lock (_thisLock)
3840
{
3941
#endif
40-
if (_innerWebSocket == null || _disposed || (_state != WebSocketState.Connecting && _state != WebSocketState.Open && _state != WebSocketState.CloseSent))
42+
if (_innerWebSocket == null || _disposed || _state == WebSocketState.Aborted || _state == WebSocketState.Closed)
4143
{
4244
return _state;
4345
}
@@ -46,15 +48,9 @@ public override WebSocketState State
4648
#endif
4749

4850
#if FEATURE_WASM_THREADS
49-
return FastState = _innerWebSocket!.SynchronizationContext.Send(static (BrowserWebSocket self) =>
50-
{
51-
lock (self._thisLock)
52-
{
53-
return GetReadyState(self._innerWebSocket!);
54-
} //lock
55-
}, this);
51+
return _innerWebSocket!.SynchronizationContext.Send(GetReadyState, this);
5652
#else
57-
return FastState = GetReadyState(_innerWebSocket!);
53+
return GetReadyState(this);
5854
#endif
5955
}
6056
}
@@ -148,7 +144,7 @@ public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType m
148144
ThrowIfDisposed();
149145

150146
// fast check of previous _state instead of GetReadyState(), the readyState would be validated on JS side
151-
if (FastState != WebSocketState.Open)
147+
if (FastState != WebSocketState.Open && FastState != WebSocketState.CloseReceived)
152148
{
153149
throw new InvalidOperationException(SR.net_WebSockets_NotConnected);
154150
}
@@ -240,7 +236,7 @@ public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string?
240236
{
241237
throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, state, "Connecting, Open, CloseSent, Aborted"));
242238
}
243-
if(state != WebSocketState.Open && state != WebSocketState.Connecting && state != WebSocketState.Aborted)
239+
if (state == WebSocketState.CloseSent)
244240
{
245241
return Task.CompletedTask;
246242
}
@@ -280,10 +276,6 @@ public override Task CloseAsync(WebSocketCloseStatus closeStatus, string? status
280276
{
281277
throw new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState, state, "Connecting, Open, CloseSent, Aborted"));
282278
}
283-
if (state != WebSocketState.Open && state != WebSocketState.Connecting && state != WebSocketState.Aborted && state != WebSocketState.CloseSent)
284-
{
285-
return Task.CompletedTask;
286-
}
287279

288280
#if FEATURE_WASM_THREADS
289281
promise = CloseAsyncCore(closeStatus, statusDescription, state != WebSocketState.Aborted, cancellationToken);
@@ -387,12 +379,13 @@ private void CreateCore(Uri uri, List<string>? requestedSubProtocols)
387379
string[]? subProtocols = requestedSubProtocols?.ToArray();
388380
var onClose = (int code, string reason) =>
389381
{
390-
_closeStatus = (WebSocketCloseStatus)code;
391-
_closeStatusDescription = reason;
392382
#if FEATURE_WASM_THREADS
393383
lock (_thisLock)
394384
{
395385
#endif
386+
_closeStatus = (WebSocketCloseStatus)code;
387+
_closeStatusDescription = reason;
388+
_closeReceived = true;
396389
WebSocketState state = State;
397390
if (state == WebSocketState.Connecting || state == WebSocketState.Open || state == WebSocketState.CloseSent)
398391
{
@@ -545,15 +538,21 @@ private static WebSocketReceiveResult ConvertResponse(BrowserWebSocket self)
545538
WebSocketMessageType messageType = (WebSocketMessageType)self.responseStatus[typeIndex];
546539
if (messageType == WebSocketMessageType.Close)
547540
{
541+
self._closeReceived = true;
542+
self.FastState = self._closeSent ? WebSocketState.Closed : WebSocketState.CloseReceived;
548543
return new WebSocketReceiveResult(self.responseStatus[countIndex], messageType, self.responseStatus[endIndex] != 0, self.CloseStatus, self.CloseStatusDescription);
549544
}
550545
return new WebSocketReceiveResult(self.responseStatus[countIndex], messageType, self.responseStatus[endIndex] != 0);
551546
}
552547

553548
private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? statusDescription, bool waitForCloseReceived, CancellationToken cancellationToken)
554549
{
555-
_closeStatus = closeStatus;
556-
_closeStatusDescription = statusDescription;
550+
if (!_closeReceived)
551+
{
552+
_closeStatus = closeStatus;
553+
_closeStatusDescription = statusDescription;
554+
}
555+
_closeSent = true;
557556

558557
var closeTask = BrowserInterop.WebSocketClose(_innerWebSocket!, (int)closeStatus, statusDescription, waitForCloseReceived) ?? Task.CompletedTask;
559558
await CancelationHelper(closeTask, cancellationToken, FastState).ConfigureAwait(true);
@@ -562,6 +561,10 @@ private async Task CloseAsyncCore(WebSocketCloseStatus closeStatus, string? stat
562561
lock (_thisLock)
563562
{
564563
#endif
564+
if (waitForCloseReceived)
565+
{
566+
_closeReceived = true;
567+
}
565568
var state = State;
566569
if (state == WebSocketState.Open || state == WebSocketState.Connecting || state == WebSocketState.CloseSent)
567570
{
@@ -614,18 +617,42 @@ private async Task CancelationHelper(Task jsTask, CancellationToken cancellation
614617
}
615618
}
616619

617-
private static WebSocketState GetReadyState(JSObject innerWebSocket)
620+
private static WebSocketState GetReadyState(BrowserWebSocket self)
618621
{
619-
var readyState = BrowserInterop.GetReadyState(innerWebSocket);
620-
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
621-
return readyState switch
622-
{
623-
0 => WebSocketState.Connecting, // 0 (CONNECTING)
624-
1 => WebSocketState.Open, // 1 (OPEN)
625-
2 => WebSocketState.CloseSent, // 2 (CLOSING)
626-
3 => WebSocketState.Closed, // 3 (CLOSED)
627-
_ => WebSocketState.None
628-
};
622+
#if FEATURE_WASM_THREADS
623+
lock (self._thisLock)
624+
{
625+
#endif
626+
var readyState = BrowserInterop.GetReadyState(self._innerWebSocket);
627+
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
628+
var st = readyState switch
629+
{
630+
0 => WebSocketState.Connecting, // 0 (CONNECTING)
631+
1 => WebSocketState.Open, // 1 (OPEN)
632+
2 => WebSocketState.CloseSent, // 2 (CLOSING)
633+
3 => WebSocketState.Closed, // 3 (CLOSED)
634+
_ => WebSocketState.None
635+
};
636+
if (st == WebSocketState.Closed || st == WebSocketState.CloseSent)
637+
{
638+
if (self._closeReceived && self._closeSent)
639+
{
640+
st = WebSocketState.Closed;
641+
}
642+
else if (self._closeReceived && !self._closeSent)
643+
{
644+
st = WebSocketState.CloseReceived;
645+
}
646+
else if (!self._closeReceived && self._closeSent)
647+
{
648+
st = WebSocketState.CloseSent;
649+
}
650+
}
651+
self.FastState = st;
652+
return st;
653+
#if FEATURE_WASM_THREADS
654+
} //lock
655+
#endif
629656
}
630657

631658
#endregion

0 commit comments

Comments
 (0)