diff --git a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs index 9a523ca701417e..5407a6095cd0ab 100644 --- a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs @@ -86,6 +86,17 @@ public void Close() CloseWebSocket(); } + public async Task WaitForCloseAsync(CancellationToken cancellationToken) + { + while (_websocket != null + ? _websocket.State != WebSocketState.Closed + : !(_socket.Poll(1, SelectMode.SelectRead) && _socket.Available == 0)) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(100); + } + } + public void Shutdown(SocketShutdown how) { _socket?.Shutdown(how); @@ -138,6 +149,9 @@ public abstract class GenericLoopbackConnection : IDisposable /// Waits for the client to signal cancellation. public abstract Task WaitForCancellationAsync(bool ignoreIncomingData = true); + /// Waits for the client to signal cancellation. + public abstract Task WaitForCloseAsync(CancellationToken cancellationToken); + /// Helper function to make it easier to convert old test with strings. public async Task SendResponseBodyAsync(string content, bool isFinal = true) { diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index b3bb701e540f36..2329475728c67b 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -984,5 +984,10 @@ public override async Task WaitForCancellationAsync(bool ignoreIncomingData = tr RstStreamFrame rstStreamFrame = Assert.IsType(frame); Assert.Equal((int)ProtocolErrors.CANCEL, rstStreamFrame.ErrorCode); } + + public override Task WaitForCloseAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackConnection.cs index ceb36e0bcbfb12..78c49a1aaee13d 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http3LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http3LoopbackConnection.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Net.Http.Functional.Tests; using Xunit; +using System.Threading; namespace System.Net.Test.Common { @@ -305,6 +306,11 @@ public override async Task WaitForCancellationAsync(bool ignoreIncomingData = tr { await GetOpenRequest().WaitForCancellationAsync(ignoreIncomingData).ConfigureAwait(false); } + + public override Task WaitForCloseAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs index 7ae59fe98e54d2..a363553ba598eb 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -28,6 +28,7 @@ public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(outp [Theory] [InlineData(false, CancellationMode.Token)] [InlineData(true, CancellationMode.Token)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/36634", TestPlatforms.Browser)] // out of memory public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode) { if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunkedTransfer) @@ -228,10 +229,21 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => await connection.ReadRequestDataAsync(); await connection.SendResponseAsync(HttpStatusCode.OK, headers: headers, isFinal: false); await clientFinished.Task; + +#if TARGET_BROWSER + // make sure that the browser closed the connection + await connection.WaitForCloseAsync(CancellationToken.None); +#endif }); var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion }; req.Headers.ConnectionClose = connectionClose; + +#if TARGET_BROWSER + var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); + req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); +#endif + Task getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token); await ValidateClientCancellationAsync(async () => { @@ -247,7 +259,6 @@ await ValidateClientCancellationAsync(async () => cts.Cancel(); await readTask; }); - try { clientFinished.SetResult(true); @@ -256,11 +267,13 @@ await ValidateClientCancellationAsync(async () => }); } } + [Theory] [InlineData(CancellationMode.CancelPendingRequests, false)] [InlineData(CancellationMode.DisposeHttpClient, false)] [InlineData(CancellationMode.CancelPendingRequests, true)] [InlineData(CancellationMode.DisposeHttpClient, true)] + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't have blocking synchronous Stream.ReadByte and so it waits for whole body")] public async Task GetAsync_CancelPendingRequests_DoesntCancelReadAsyncOnResponseStream(CancellationMode mode, bool copyToAsync) { if (IsWinHttpHandler && UseVersion >= HttpVersion20.Value) diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs index 30cb5d4a3f0431..70ddfcf1f5aaf6 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs @@ -1065,6 +1065,11 @@ public override async Task WaitForCancellationAsync(bool ignoreIncomingData = tr } } } + + public override Task WaitForCloseAsync(CancellationToken cancellationToken) + { + return _socket.WaitForCloseAsync(cancellationToken); + } } public override async Task HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList headers = null, string content = "") diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/RemoteLoopServer/Handlers/RemoteLoopHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/RemoteLoopServer/Handlers/RemoteLoopHandler.cs index 4f83c67b0b30f4..557eaa494f7c78 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/RemoteLoopServer/Handlers/RemoteLoopHandler.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/RemoteLoopServer/Handlers/RemoteLoopHandler.cs @@ -100,6 +100,11 @@ private static async Task ProcessWebSocketRequest(HttpContext context, WebSocket var slice = new ArraySegment(testedBuffer, 0, testedNext.Result); await control.SendAsync(slice, WebSocketMessageType.Binary, true, cts.Token).ConfigureAwait(false); } + // did we get TCP FIN? + if (!close && (tested.Poll(1, SelectMode.SelectRead) && tested.Available == 0)) + { + close = true; + } if (!close) { testedNext = tested.ReceiveAsync(new Memory(testedBuffer), SocketFlags.None, cts.Token).AsTask(); @@ -142,14 +147,14 @@ private static async Task ProcessWebSocketRequest(HttpContext context, WebSocket } catch (WebSocketException ex) { - logger.LogWarning("ProcessWebSocketRequest closing failed", ex); + logger.LogWarning("RemoteLoopHandler.ProcessWebSocketRequest closing failed", ex); } } cts.Cancel(); } catch (Exception ex) { - logger.LogError("ProcessWebSocketRequest failed", ex); + logger.LogError("RemoteLoopHandler.ProcessWebSocketRequest failed", ex); } finally { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs index 8d022091ea3961..b03247fbfc3d7e 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs @@ -148,10 +148,10 @@ protected internal override async Task SendAsync(HttpReques { throw new ArgumentNullException(nameof(request), SR.net_http_handler_norequest); } - + CancellationTokenRegistration? abortRegistration = null; try { - var requestObject = new JSObject(); + using var requestObject = new JSObject(); if (request.Options.TryGetValue(FetchOptions, out IDictionary? fetchOptions)) { @@ -221,44 +221,39 @@ protected internal override async Task SendAsync(HttpReques } - WasmHttpReadStream? wasmHttpReadStream = null; - JSObject abortController = new HostObject("AbortController"); - JSObject signal = (JSObject)abortController.GetObjectProperty("signal"); + using JSObject signal = (JSObject)abortController.GetObjectProperty("signal"); requestObject.SetObjectProperty("signal", signal); - signal.Dispose(); - CancellationTokenSource abortCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - CancellationTokenRegistration abortRegistration = abortCts.Token.Register((Action)(() => + abortRegistration = cancellationToken.Register(() => { - if (abortController.JSHandle != -1) + if (!abortController.IsDisposed) { abortController.Invoke("abort"); abortController?.Dispose(); } - wasmHttpReadStream?.Dispose(); - abortCts.Dispose(); - })); + }); - var args = new System.Runtime.InteropServices.JavaScript.Array(); + using var args = new System.Runtime.InteropServices.JavaScript.Array(); if (request.RequestUri != null) { args.Push(request.RequestUri.ToString()); args.Push(requestObject); } - requestObject.Dispose(); - var response = s_fetch?.Invoke("apply", s_window, args) as Task; - args.Dispose(); - if (response == null) + var responseTask = s_fetch?.Invoke("apply", s_window, args) as Task; + if (responseTask == null) throw new Exception(SR.net_http_marshalling_response_promise_from_fetch); - JSObject t = (JSObject)await response.ConfigureAwait(continueOnCapturedContext: true); + cancellationToken.ThrowIfCancellationRequested(); - var status = new WasmFetchResponse(t, abortController, abortCts, abortRegistration); - HttpResponseMessage httpResponse = new HttpResponseMessage((HttpStatusCode)status.Status); - httpResponse.RequestMessage = request; + var fetchResponseJs = (JSObject)await responseTask.ConfigureAwait(continueOnCapturedContext: true); + + var fetchResponse = new WasmFetchResponse(fetchResponseJs, abortController, abortRegistration.Value); + abortRegistration = null; + var responseMessage = new HttpResponseMessage((HttpStatusCode)fetchResponse.Status); + responseMessage.RequestMessage = request; // Here we will set the ReasonPhrase so that it can be evaluated later. // We do not have a status code but this will signal some type of what happened @@ -267,9 +262,9 @@ protected internal override async Task SendAsync(HttpReques // https://developer.mozilla.org/en-US/docs/Web/API/Response/type // opaqueredirect: The fetch request was made with redirect: "manual". // The Response's status is 0, headers are empty, body is null and trailer is empty. - if (status.ResponseType == "opaqueredirect") + if (fetchResponse.ResponseType == "opaqueredirect") { - httpResponse.SetReasonPhraseWithoutValidation(status.ResponseType); + responseMessage.SetReasonPhraseWithoutValidation(fetchResponse.ResponseType); } bool streamingEnabled = false; @@ -278,9 +273,9 @@ protected internal override async Task SendAsync(HttpReques request.Options.TryGetValue(EnableStreamingResponse, out streamingEnabled); } - httpResponse.Content = streamingEnabled - ? new StreamContent(wasmHttpReadStream = new WasmHttpReadStream(status)) - : (HttpContent)new BrowserHttpContent(status); + responseMessage.Content = streamingEnabled + ? new StreamContent(new WasmHttpReadStream(fetchResponse)) + : new BrowserHttpContent(fetchResponse); // Fill the response headers // CORS will only allow access to certain headers. @@ -290,7 +285,7 @@ protected internal override async Task SendAsync(HttpReques // View more information https://developers.google.com/web/updates/2015/03/introduction-to-fetch#response_types // // Note: Some of the headers may not even be valid header types in .NET thus we use TryAddWithoutValidation - using (JSObject respHeaders = status.Headers) + using (JSObject respHeaders = fetchResponse.Headers) { if (respHeaders != null) { @@ -306,8 +301,8 @@ protected internal override async Task SendAsync(HttpReques { var name = (string)resultValue[0]; var value = (string)resultValue[1]; - if (!httpResponse.Headers.TryAddWithoutValidation(name, value)) - httpResponse.Content.Headers.TryAddWithoutValidation(name, value); + if (!responseMessage.Headers.TryAddWithoutValidation(name, value)) + responseMessage.Content.Headers.TryAddWithoutValidation(name, value); } nextResult?.Dispose(); nextResult = (JSObject)entriesIterator.Invoke("next"); @@ -320,7 +315,7 @@ protected internal override async Task SendAsync(HttpReques } } } - return httpResponse; + return responseMessage; } catch (OperationCanceledException oce) when (cancellationToken.IsCancellationRequested) @@ -331,6 +326,10 @@ protected internal override async Task SendAsync(HttpReques { throw TranslateJSException(jse, cancellationToken); } + finally + { + abortRegistration?.Dispose(); + } } private static Exception TranslateJSException(JSException jse, CancellationToken cancellationToken) @@ -350,15 +349,13 @@ private sealed class WasmFetchResponse : IDisposable { private readonly JSObject _fetchResponse; private readonly JSObject _abortController; - private readonly CancellationTokenSource _abortCts; private readonly CancellationTokenRegistration _abortRegistration; private bool _isDisposed; - public WasmFetchResponse(JSObject fetchResponse, JSObject abortController, CancellationTokenSource abortCts, CancellationTokenRegistration abortRegistration) + public WasmFetchResponse(JSObject fetchResponse, JSObject abortController, CancellationTokenRegistration abortRegistration) { _fetchResponse = fetchResponse ?? throw new ArgumentNullException(nameof(fetchResponse)); _abortController = abortController ?? throw new ArgumentNullException(nameof(abortController)); - _abortCts = abortCts; _abortRegistration = abortRegistration; } @@ -383,10 +380,13 @@ public void Dispose() _isDisposed = true; - _abortCts.Dispose(); _abortRegistration.Dispose(); _fetchResponse?.Dispose(); + if (_abortController != null && !_abortController.IsDisposed) + { + _abortController.Invoke("abort"); + } _abortController?.Dispose(); } } @@ -460,15 +460,15 @@ protected override void Dispose(bool disposing) private sealed class WasmHttpReadStream : Stream { - private WasmFetchResponse? _status; + private WasmFetchResponse? _fetchResponse; private JSObject? _reader; private byte[]? _bufferedBytes; private int _position; - public WasmHttpReadStream(WasmFetchResponse status) + public WasmHttpReadStream(WasmFetchResponse fetchResponse) { - _status = status; + _fetchResponse = fetchResponse; } public override bool CanRead => true; @@ -489,17 +489,19 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) { + CancellationHelper.ThrowIfCancellationRequested(cancellationToken); + if (_reader == null) { // If we've read everything, then _reader and _status will be null - if (_status == null) + if (_fetchResponse == null) { return 0; } try { - using (JSObject body = _status.Body) + using (JSObject body = _fetchResponse.Body) { _reader = (JSObject)body.Invoke("getReader"); } @@ -514,6 +516,11 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation } } + using var abortRegistration = cancellationToken.Register(() => + { + _reader.Invoke("cancel"); + }); + if (_bufferedBytes != null && _position < _bufferedBytes.Length) { return ReadBuffered(); @@ -524,13 +531,19 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation var t = (Task)_reader.Invoke("read"); using (var read = (JSObject)await t.ConfigureAwait(continueOnCapturedContext: true)) { + if (cancellationToken.IsCancellationRequested) + { + _reader.Invoke("cancel"); + throw CancellationHelper.CreateOperationCanceledException(null, cancellationToken); + } + if ((bool)read.GetObjectProperty("done")) { _reader.Dispose(); _reader = null; - _status?.Dispose(); - _status = null; + _fetchResponse?.Dispose(); + _fetchResponse = null; return 0; } @@ -569,7 +582,7 @@ int ReadBuffered() protected override void Dispose(bool disposing) { _reader?.Dispose(); - _status?.Dispose(); + _fetchResponse?.Dispose(); } public override void Flush() diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs index 8431f2565e8195..523c25c83bed99 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/SocketsHttpHandlerTest.cs @@ -1044,12 +1044,12 @@ public sealed class SocketsHttpHandlerTest_Cookies_Http11 : HttpClientHandlerTes public SocketsHttpHandlerTest_Cookies_Http11(ITestOutputHelper output) : base(output) { } } - [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public sealed class SocketsHttpHandler_HttpClientHandler_Http11_Cancellation_Test : SocketsHttpHandler_Cancellation_Test { public SocketsHttpHandler_HttpClientHandler_Http11_Cancellation_Test(ITestOutputHelper output) : base(output) { } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void ConnectTimeout_Default() { using (var handler = new SocketsHttpHandler()) @@ -1062,6 +1062,7 @@ public void ConnectTimeout_Default() [InlineData(0)] [InlineData(-2)] [InlineData(int.MaxValue + 1L)] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void ConnectTimeout_InvalidValues(long ms) { using (var handler = new SocketsHttpHandler()) @@ -1075,6 +1076,7 @@ public void ConnectTimeout_InvalidValues(long ms) [InlineData(1)] [InlineData(int.MaxValue - 1)] [InlineData(int.MaxValue)] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void ConnectTimeout_ValidValues_Roundtrip(long ms) { using (var handler = new SocketsHttpHandler()) @@ -1085,6 +1087,7 @@ public void ConnectTimeout_ValidValues_Roundtrip(long ms) } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void ConnectTimeout_SetAfterUse_Throws() { using (var handler = new SocketsHttpHandler()) @@ -1098,6 +1101,7 @@ public void ConnectTimeout_SetAfterUse_Throws() } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void Expect100ContinueTimeout_Default() { using (var handler = new SocketsHttpHandler()) @@ -1109,6 +1113,7 @@ public void Expect100ContinueTimeout_Default() [Theory] [InlineData(-2)] [InlineData(int.MaxValue + 1L)] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void Expect100ContinueTimeout_InvalidValues(long ms) { using (var handler = new SocketsHttpHandler()) @@ -1122,6 +1127,7 @@ public void Expect100ContinueTimeout_InvalidValues(long ms) [InlineData(1)] [InlineData(int.MaxValue - 1)] [InlineData(int.MaxValue)] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void Expect100ContinueTimeout_ValidValues_Roundtrip(long ms) { using (var handler = new SocketsHttpHandler()) @@ -1132,6 +1138,7 @@ public void Expect100ContinueTimeout_ValidValues_Roundtrip(long ms) } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "ConnectTimeout is not supported on Browser")] public void Expect100ContinueTimeout_SetAfterUse_Throws() { using (var handler = new SocketsHttpHandler())