From 4b9af8e3fb57179e5a7c88b89ea525ef264f73ac Mon Sep 17 00:00:00 2001 From: campersau Date: Tue, 21 Jan 2025 22:29:58 +0100 Subject: [PATCH] [browser] [wasm] Make response streaming opt-out --- .../HttpClientHandlerTest.Cancellation.cs | 5 --- .../System/Net/Http/HttpClientHandlerTest.cs | 45 ++++++++++--------- .../System/Net/Http/ResponseStreamTest.cs | 19 ++++---- .../BrowserHttpHandler/BrowserHttpHandler.cs | 18 +++----- .../JavaScript/WebWorkerTest.Http.cs | 2 - 5 files changed, 40 insertions(+), 49 deletions(-) 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 c5bed9f4567390..b0a274ad787fcf 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs @@ -244,11 +244,6 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) => 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 () => { diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs index b45f2a5b232b72..946381dee0b4b8 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs @@ -1005,12 +1005,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; if (PlatformDetection.IsBrowser) { - if (enableWasmStreaming) - { #if !NETFRAMEWORK - request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true); + request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), enableWasmStreaming); #endif - } } using (var client = new HttpMessageInvoker(CreateHttpClientHandler())) @@ -1239,7 +1236,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => // Boolean properties returning correct values Assert.True(responseStream.CanRead); Assert.False(responseStream.CanWrite); - Assert.Equal(PlatformDetection.IsBrowser, responseStream.CanSeek); + Assert.False(responseStream.CanSeek); // Not supported operations Assert.Throws(() => responseStream.BeginWrite(new byte[1], 0, 1, null, null)); @@ -1270,11 +1267,14 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => Assert.Throws(() => { responseStream.CopyToAsync(Stream.Null, -1, default); }); Assert.Throws(() => { responseStream.CopyToAsync(nonWritableStream, 100, default); }); Assert.Throws(() => { responseStream.CopyToAsync(disposedStream, 100, default); }); - Assert.Throws(() => responseStream.Read(null, 0, 100)); - Assert.Throws(() => responseStream.Read(new byte[1], -1, 1)); - Assert.ThrowsAny(() => responseStream.Read(new byte[1], 2, 1)); - Assert.Throws(() => responseStream.Read(new byte[1], 0, -1)); - Assert.ThrowsAny(() => responseStream.Read(new byte[1], 0, 2)); + if (PlatformDetection.IsNotBrowser) + { + Assert.Throws(() => responseStream.Read(null, 0, 100)); + Assert.Throws(() => responseStream.Read(new byte[1], -1, 1)); + Assert.ThrowsAny(() => responseStream.Read(new byte[1], 2, 1)); + Assert.Throws(() => responseStream.Read(new byte[1], 0, -1)); + Assert.ThrowsAny(() => responseStream.Read(new byte[1], 0, 2)); + } Assert.Throws(() => responseStream.BeginRead(null, 0, 100, null, null)); Assert.Throws(() => responseStream.BeginRead(new byte[1], -1, 1, null, null)); Assert.ThrowsAny(() => responseStream.BeginRead(new byte[1], 2, 1, null, null)); @@ -1284,29 +1284,37 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri => Assert.Throws(() => { responseStream.CopyTo(null); }); Assert.Throws(() => { responseStream.CopyToAsync(null, 100, default); }); Assert.Throws(() => { responseStream.CopyToAsync(null, 100, default); }); - Assert.Throws(() => { responseStream.Read(null, 0, 100); }); Assert.Throws(() => { responseStream.ReadAsync(null, 0, 100, default); }); Assert.Throws(() => { responseStream.BeginRead(null, 0, 100, null, null); }); // Empty reads var buffer = new byte[1]; - Assert.Equal(-1, responseStream.ReadByte()); - Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null)); + if (PlatformDetection.IsNotBrowser) + { + Assert.Equal(-1, responseStream.ReadByte()); + Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null)); + } #if !NETFRAMEWORK Assert.Equal(0, await responseStream.ReadAsync(new Memory(buffer))); #endif Assert.Equal(0, await responseStream.ReadAsync(buffer, 0, 1)); + if (PlatformDetection.IsNotBrowser) + { #if !NETFRAMEWORK - Assert.Equal(0, responseStream.Read(new Span(buffer))); + Assert.Equal(0, responseStream.Read(new Span(buffer))); #endif - Assert.Equal(0, responseStream.Read(buffer, 0, 1)); + Assert.Equal(0, responseStream.Read(buffer, 0, 1)); + } // Empty copies var ms = new MemoryStream(); await responseStream.CopyToAsync(ms); Assert.Equal(0, ms.Length); - responseStream.CopyTo(ms); - Assert.Equal(0, ms.Length); + if (PlatformDetection.IsNotBrowser) + { + responseStream.CopyTo(ms); + Assert.Equal(0, ms.Length); + } } } }, @@ -1322,9 +1330,6 @@ public async Task ReadAsStreamAsync_StreamingCancellation() await LoopbackServerFactory.CreateClientAndServerAsync(async uri => { var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion }; -#if !NETFRAMEWORK - request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true); -#endif var cts = new CancellationTokenSource(); using (var client = new HttpMessageInvoker(CreateHttpClientHandler())) diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index 7f58fd5b2424e8..5fac124e98e428 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -25,6 +25,8 @@ public static IEnumerable RemoteServersAndReadModes() { for (int i = 0; i < 8; i++) { + if (PlatformDetection.IsBrowser && i is 0 or 2 or 4 or 5 or 6) continue; // ignore sync reads + yield return new object[] { remoteServer, i }; } } @@ -176,10 +178,13 @@ public async Task GetStreamAsync_ReadZeroBytes_Success(Configuration.Http.Remote using (HttpClient client = CreateHttpClientForRemoteServer(remoteServer)) using (Stream stream = await client.GetStreamAsync(remoteServer.EchoUri)) { - Assert.Equal(0, stream.Read(new byte[1], 0, 0)); + if (PlatformDetection.IsNotBrowser) + { + Assert.Equal(0, stream.Read(new byte[1], 0, 0)); #if !NETFRAMEWORK - Assert.Equal(0, stream.Read(new Span(new byte[1], 0, 0))); + Assert.Equal(0, stream.Read(new Span(new byte[1], 0, 0))); #endif + } Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 0)); } } @@ -200,7 +205,7 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders cts.Cancel(); // Verify that the task completed. - Assert.True(((IAsyncResult)task).AsyncWaitHandle.WaitOne(new TimeSpan(0, 5, 0))); + Assert.Same(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromMinutes(5)))); Assert.True(task.IsCompleted, "Task was not yet completed"); // Verify that the task completed successfully or is canceled. @@ -234,12 +239,10 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders public async Task BrowserHttpHandler_Streaming() { var WebAssemblyEnableStreamingRequestKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingRequest"); - var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); var req = new HttpRequestMessage(HttpMethod.Post, Configuration.Http.RemoteHttp2Server.BaseUri + "echobody.ashx"); req.Options.Set(WebAssemblyEnableStreamingRequestKey, true); - req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); byte[] body = new byte[1024 * 1024]; Random.Shared.NextBytes(body); @@ -488,13 +491,9 @@ public async Task BrowserHttpHandler_StreamingRequest_Http1Fails() [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))] public async Task BrowserHttpHandler_StreamingResponse() { - var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); - var size = 1500 * 1024 * 1024; var req = new HttpRequestMessage(HttpMethod.Get, Configuration.Http.RemoteSecureHttp11Server.BaseUri + "large.ashx?size=" + size); - req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); - using (HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteSecureHttp11Server)) // we need to switch off Response buffering of default ResponseContentRead option using (HttpResponseMessage response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead)) @@ -512,7 +511,7 @@ public async Task BrowserHttpHandler_StreamingResponse() int fetchedCount = 0; do { - // with WebAssemblyEnableStreamingResponse option set, we will be using https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read + // we will be using https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read fetchedCount = await stream.ReadAsync(buffer, 0, buffer.Length); totalCount += fetchedCount; } while (fetchedCount != 0); 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 6b74d5d9cafed8..e1476de86c533a 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 @@ -143,7 +143,7 @@ internal sealed class BrowserHttpController : IDisposable private readonly string uri; private readonly CancellationToken _cancellationToken; private readonly HttpRequestMessage _request; - private bool _isDisposed; + internal bool _isDisposed; public BrowserHttpController(HttpRequestMessage request, bool? allowAutoRedirect, CancellationToken cancellationToken) { @@ -314,13 +314,7 @@ private HttpResponseMessage ConvertResponse() responseMessage.SetReasonPhraseWithoutValidation(responseType); } - bool streamingResponseEnabled = false; - if (BrowserHttpInterop.SupportsStreamingResponse()) - { - _request.Options.TryGetValue(EnableStreamingResponse, out streamingResponseEnabled); - } - - responseMessage.Content = streamingResponseEnabled + responseMessage.Content = (_request.Options.TryGetValue(EnableStreamingResponse, out var streamingResponseEnabled) ? streamingResponseEnabled : true) && BrowserHttpInterop.SupportsStreamingResponse() ? new StreamContent(new BrowserHttpReadStream(this)) : new BrowserHttpContent(this); @@ -361,10 +355,10 @@ public void Dispose() internal sealed class BrowserHttpWriteStream : Stream { private readonly BrowserHttpController _controller; // we don't own it, we don't dispose it from here + public BrowserHttpWriteStream(BrowserHttpController controller) { ArgumentNullException.ThrowIfNull(controller); - _controller = controller; } @@ -392,7 +386,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati public override bool CanRead => false; public override bool CanSeek => false; - public override bool CanWrite => true; + public override bool CanWrite => !_controller._isDisposed; protected override void Dispose(bool disposing) { @@ -506,7 +500,7 @@ protected override void Dispose(bool disposing) internal sealed class BrowserHttpReadStream : Stream { - private BrowserHttpController _controller; // we own the object and have to dispose it + private readonly BrowserHttpController _controller; // we own the object and have to dispose it public BrowserHttpReadStream(BrowserHttpController controller) { @@ -540,7 +534,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel return ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask(); } - public override bool CanRead => true; + public override bool CanRead => !_controller._isDisposed; public override bool CanSeek => false; public override bool CanWrite => false; diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs index 56505353cff6f6..4550bef70dd4f5 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs @@ -32,7 +32,6 @@ await executor.Execute(async () => } private static HttpRequestOptionsKey WebAssemblyEnableStreamingRequestKey = new("WebAssemblyEnableStreamingRequest"); - private static HttpRequestOptionsKey WebAssemblyEnableStreamingResponseKey = new("WebAssemblyEnableStreamingResponse"); private static string HelloJson = "{'hello':'world'}".Replace('\'', '"'); private static string EchoStart = "{\"Method\":\"POST\",\"Url\":\"/Echo.ashx"; @@ -46,7 +45,6 @@ private async Task HttpClient_ActionInDifferentThread(string url, Executor execu await ms.WriteAsync(Encoding.UTF8.GetBytes(HelloJson)); using var req = new HttpRequestMessage(HttpMethod.Post, url); - req.Options.Set(WebAssemblyEnableStreamingResponseKey, true); req.Content = new StreamContent(ms); using var client = new HttpClient(); var pr = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);