From 4b9af8e3fb57179e5a7c88b89ea525ef264f73ac Mon Sep 17 00:00:00 2001 From: campersau Date: Tue, 21 Jan 2025 22:29:58 +0100 Subject: [PATCH 1/5] [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); From 1f96a830bdf9ad04ebc51a25b0624b0186b2ea84 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 7 Mar 2025 18:36:40 +0100 Subject: [PATCH 2/5] - WebAssemblyEnableStreamingResponse msbuild property - System.Net.Http.WasmEnableStreamingResponse linker feature - DOTNET_WASM_ENABLE_STREAMING_RESPONSE env variable - tests & docs --- .../System/Net/Http/ResponseStreamTest.cs | 2 +- .../src/ILLink/ILLink.Substitutions.xml | 3 ++ .../BrowserHttpHandler/BrowserHttpHandler.cs | 36 ++++++++++++++++++- .../BrowserHttpHandler/BrowserHttpInterop.cs | 19 ++++++++-- ...me.InteropServices.JavaScript.Tests.csproj | 1 + .../JavaScript/HttpRequestMessageTest.cs | 27 ++++++++++++++ src/mono/browser/browser.proj | 2 ++ src/mono/browser/build/BrowserWasmApp.targets | 10 ++++++ .../WasmFeatures.props | 1 + src/mono/wasm/features.md | 8 +++++ 10 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs index 5fac124e98e428..500228642ea625 100644 --- a/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs +++ b/src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs @@ -488,7 +488,7 @@ public async Task BrowserHttpHandler_StreamingRequest_Http1Fails() } [OuterLoop] - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))] + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] public async Task BrowserHttpHandler_StreamingResponse() { var size = 1500 * 1024 * 1024; diff --git a/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml b/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml index 5e70f6fbb5d7d5..d99263515cae71 100644 --- a/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml +++ b/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml @@ -3,5 +3,8 @@ + + + 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 df063e70197c82..b2e73bc1cef364 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 @@ -9,6 +9,7 @@ using System.Runtime.InteropServices.JavaScript; using System.Threading; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; namespace System.Net.Http { @@ -134,6 +135,9 @@ internal sealed class BrowserHttpController : IDisposable private static readonly HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); private static readonly HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); + [FeatureSwitchDefinition("System.Net.Http.WasmEnableStreamingResponse")] + internal static bool FeatureEnableStreamingResponse { get; } = AppContextConfigHelper.GetBooleanConfig("System.Net.Http.WasmEnableStreamingResponse", "DOTNET_WASM_ENABLE_STREAMING_RESPONSE", defaultValue: true); + internal readonly JSObject _jsController; private readonly CancellationTokenRegistration _abortRegistration; private readonly string[] _optionNames; @@ -314,7 +318,17 @@ private HttpResponseMessage ConvertResponse() responseMessage.SetReasonPhraseWithoutValidation(responseType); } - responseMessage.Content = (_request.Options.TryGetValue(EnableStreamingResponse, out var streamingResponseEnabled) ? streamingResponseEnabled : true) && BrowserHttpInterop.SupportsStreamingResponse() + bool streamingResponseEnabled = FeatureEnableStreamingResponse; + if (_request.Options.TryGetValue(EnableStreamingResponse, out var reqStreamingResponseEnabled)) + { + streamingResponseEnabled = reqStreamingResponseEnabled; + } + if (streamingResponseEnabled && !BrowserHttpInterop.SupportsStreamingResponse()) + { + throw new PlatformNotSupportedException("Streaming response is not supported in this browser."); + } + + responseMessage.Content = streamingResponseEnabled ? new StreamContent(new BrowserHttpReadStream(this)) : new BrowserHttpContent(this); @@ -576,4 +590,24 @@ public override void Write(byte[] buffer, int offset, int count) } #endregion } + + internal static class AppContextConfigHelper + { + internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false) + { + string? str = Environment.GetEnvironmentVariable(envVariable); + if (str != null) + { + if (str == "1" || str.Equals("true", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (str == "0" || str.Equals("false", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + return AppContext.TryGetSwitch(switchName, out bool value) ? value : defaultValue; + } + } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs index 92f3024df33161..517a23060c0164 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs @@ -11,11 +11,26 @@ namespace System.Net.Http { internal static partial class BrowserHttpInterop { + private static bool? _SupportsStreamingRequest; + private static bool? _SupportsStreamingResponse; + + public static bool SupportsStreamingRequest() + { + _SupportsStreamingRequest ??= SupportsStreamingRequestImpl(); + return _SupportsStreamingRequest.Value; + } + + public static bool SupportsStreamingResponse() + { + _SupportsStreamingResponse ??= SupportsStreamingResponseImpl(); + return _SupportsStreamingResponse.Value; + } + [JSImport("INTERNAL.http_wasm_supports_streaming_request")] - public static partial bool SupportsStreamingRequest(); + public static partial bool SupportsStreamingRequestImpl(); [JSImport("INTERNAL.http_wasm_supports_streaming_response")] - public static partial bool SupportsStreamingResponse(); + public static partial bool SupportsStreamingResponseImpl(); [JSImport("INTERNAL.http_wasm_create_controller")] public static partial JSObject CreateController(); diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj index 846442cca6ca1c..5cb23d8045f7f1 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj @@ -20,6 +20,7 @@ $(NoWarn);NU1511 + false diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/HttpRequestMessageTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/HttpRequestMessageTest.cs index 815dfeb0dcd2df..9fc567c64bb2ab 100644 --- a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/HttpRequestMessageTest.cs +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/HttpRequestMessageTest.cs @@ -16,6 +16,8 @@ namespace System.Runtime.InteropServices.JavaScript.Http.Tests { public class HttpRequestMessageTest { + public static readonly string LocalHttpEcho = "http://" + Environment.GetEnvironmentVariable("DOTNET_TEST_HTTPHOST") + "/Echo.ashx"; + private readonly Version _expectedRequestMessageVersion = HttpVersion.Version11; private HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); private HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); @@ -295,6 +297,31 @@ public void Properties_SetOptionsAndGetTheirValue_NotSet_EnableStreamingResponse Assert.False(streamingEnabledValue); } + [Fact] + public async Task HttpStreamingDisabledBy_WasmEnableStreamingResponse_InProject() + { + using var client = new HttpClient(); + using var req = new HttpRequestMessage(HttpMethod.Get, LocalHttpEcho + "?guid=" + Guid.NewGuid()); + using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("BrowserHttpContent", response.Content.GetType().Name); + using var stream = await response.Content.ReadAsStreamAsync(); + Assert.Equal("MemoryStream", stream.GetType().Name); + Assert.True(stream.CanSeek); + } + + [Fact] + public async Task HttpStreamingEnabledBy_WebAssemblyEnableStreamingResponse_Option() + { + using var client = new HttpClient(); + using var req = new HttpRequestMessage(HttpMethod.Get, LocalHttpEcho + "?guid=" + Guid.NewGuid()); + req.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true); + using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead); + Assert.Equal("StreamContent", response.Content.GetType().Name); + using var stream = await response.Content.ReadAsStreamAsync(); + Assert.Equal("ReadOnlyStream", stream.GetType().Name); + Assert.False(stream.CanSeek); + } + [Fact] public void Version_SetToNull_ThrowsArgumentNullException() { diff --git a/src/mono/browser/browser.proj b/src/mono/browser/browser.proj index b82bfdf21a5ad8..596d34ddfc830a 100644 --- a/src/mono/browser/browser.proj +++ b/src/mono/browser/browser.proj @@ -19,6 +19,7 @@ $([MSBuild]::NormalizeDirectory('$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)', 'runtimes', 'browser-wasm', 'native', 'lib')) $([MSBuild]::NormalizeDirectory('$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)', 'runtimes', 'browser-wasm-threads', 'native', 'lib')) false + true false true false @@ -336,6 +337,7 @@ "PropertiesThatTriggerRelinking": [ { "identity": "InvariantTimezone", "defaultValueInRuntimePack": "$(InvariantTimezone)" }, { "identity": "InvariantGlobalization", "defaultValueInRuntimePack": "$(InvariantGlobalization)" }, + { "identity": "WasmEnableStreamingResponse", "defaultValueInRuntimePack": "$(WasmEnableStreamingResponse)" }, { "identity": "WasmNativeStrip", "defaultValueInRuntimePack": "$(WasmNativeStrip)" }, { "identity": "WasmSingleFileBundle", "defaultValueInRuntimePack": "$(WasmSingleFileBundle)" }, { "identity": "WasmEnableSIMD", "defaultValueInRuntimePack": "$(WasmEnableSIMD)" }, diff --git a/src/mono/browser/build/BrowserWasmApp.targets b/src/mono/browser/build/BrowserWasmApp.targets index bb7c499c91967b..291d5bcd3b4c0c 100644 --- a/src/mono/browser/build/BrowserWasmApp.targets +++ b/src/mono/browser/build/BrowserWasmApp.targets @@ -58,8 +58,18 @@ <_WasmDefaultFlags Condition="'$(WasmEnableSIMD)' == 'true'">-msimd128 <_WasmOutputFileName Condition="'$(WasmSingleFileBundle)' != 'true'">dotnet.native.wasm + + true + + + + + diff --git a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WasmFeatures.props b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WasmFeatures.props index 289b8c503a39fc..84b0ab729466b0 100644 --- a/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WasmFeatures.props +++ b/src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WasmFeatures.props @@ -8,5 +8,6 @@ false false false + true diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index ec00eff02e3d6a..e96c7d6dd759de 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -55,6 +55,14 @@ Because web browsers do not expose direct access to sockets, we are unable to pr A prominent limitation is that your application must obey `Cross-Origin Resource Sharing` (CORS) rules in order to perform network requests successfully - see [CORS on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) for more information. +Since Net 10 Preview 3 the HTTP client supports [streaming HTTP response](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#consuming_a_fetch_as_a_stream) by default because all evergreen browsers now support it. + +This is a breaking change because the `response.Content.ReadAsStreamAsync()` is no longer `MemoryStream` but `BrowserHttpReadStream` which doesn't support synchronous operations like `Stream.Read(Span)`. If your code uses synchronous operations, you can disable the feature or copy the stream into `MemoryStream` yourself. + +If you need to disable it, you can use `false` or `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` env variable to do it for all HTTP requests. + +Or you can use `request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true);` for individual request. + ### WebSocket Applications using the [WebSocketClient](https://learn.microsoft.com/dotnet/api/system.net.websockets.clientwebsocket) managed API will require the browser to support the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) API. From 019b411a74ee49f8427f2410fc0815f16671d1c8 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 10 Mar 2025 13:14:32 +0100 Subject: [PATCH 3/5] fix linker ? --- .../System.Net.Http/src/ILLink/ILLink.Substitutions.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml b/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml index d99263515cae71..c72675a6a35e8c 100644 --- a/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml +++ b/src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml @@ -3,8 +3,10 @@ + + - + From 770f69ac201fa91277dc7746940ea9ac82f794d9 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Tue, 11 Mar 2025 09:59:23 +0100 Subject: [PATCH 4/5] Update src/mono/wasm/features.md Co-authored-by: campersau --- src/mono/wasm/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index e96c7d6dd759de..6f70c8ec98609e 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -61,7 +61,7 @@ This is a breaking change because the `response.Content.ReadAsStreamAsync()` is If you need to disable it, you can use `false` or `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` env variable to do it for all HTTP requests. -Or you can use `request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), true);` for individual request. +Or you can use `request.Options.Set(new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"), false);` for individual request. ### WebSocket Applications using the [WebSocketClient](https://learn.microsoft.com/dotnet/api/system.net.websockets.clientwebsocket) managed API will require the browser to support the [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) API. From 093008643d80f95fe9503fb6958e70d4c70a4a93 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 11 Mar 2025 10:03:59 +0100 Subject: [PATCH 5/5] feedback --- src/mono/browser/runtime/http.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/mono/browser/runtime/http.ts b/src/mono/browser/runtime/http.ts index 74f86f88791c4d..149c3c05a473f6 100644 --- a/src/mono/browser/runtime/http.ts +++ b/src/mono/browser/runtime/http.ts @@ -26,11 +26,7 @@ function commonAsserts (controller: HttpController) { mono_assert(controller, "expected controller"); } -let http_wasm_supports_streaming_request_cached: boolean | undefined; export function http_wasm_supports_streaming_request (): boolean { - if (http_wasm_supports_streaming_request_cached !== undefined) { - return http_wasm_supports_streaming_request_cached; - } // Detecting streaming request support works like this: // If the browser doesn't support a particular body type, it calls toString() on the object and uses the result as the body. // So, if the browser doesn't support request streams, the request body becomes the string "[object ReadableStream]". @@ -48,20 +44,13 @@ export function http_wasm_supports_streaming_request (): boolean { return "half"; }, } as RequestInit /* https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483 */).headers.has("Content-Type"); - http_wasm_supports_streaming_request_cached = duplexAccessed && !hasContentType; - } else { - http_wasm_supports_streaming_request_cached = false; + return duplexAccessed && !hasContentType; } - return http_wasm_supports_streaming_request_cached; + return false; } -let http_wasm_supports_streaming_response_cached: boolean | undefined; export function http_wasm_supports_streaming_response (): boolean { - if (http_wasm_supports_streaming_response_cached !== undefined) { - return http_wasm_supports_streaming_response_cached; - } - http_wasm_supports_streaming_response_cached = typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function"; - return http_wasm_supports_streaming_response_cached; + return typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function"; } export function http_wasm_create_controller (): HttpController {