Skip to content

Commit e12a753

Browse files
campersaupavelsavara
andauthoredMar 17, 2025··
[browser][wasm] Make response streaming opt-out (#111680)
Co-authored-by: campersau <buchholz.bastian@googlemail.com> Co-authored-by: pavelsavara <pavel.savara@gmail.com>
1 parent 5a54b6d commit e12a753

File tree

14 files changed

+145
-62
lines changed

14 files changed

+145
-62
lines changed
 

‎src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs

-5
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,6 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
244244
var req = new HttpRequestMessage(HttpMethod.Get, url) { Version = UseVersion };
245245
req.Headers.ConnectionClose = connectionClose;
246246

247-
#if TARGET_BROWSER
248-
var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
249-
req.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
250-
#endif
251-
252247
Task<HttpResponseMessage> getResponse = client.SendAsync(TestAsync, req, HttpCompletionOption.ResponseHeadersRead, cts.Token);
253248
await ValidateClientCancellationAsync(async () =>
254249
{

‎src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.cs

+25-20
Original file line numberDiff line numberDiff line change
@@ -1005,12 +1005,9 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
10051005
var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion };
10061006
if (PlatformDetection.IsBrowser)
10071007
{
1008-
if (enableWasmStreaming)
1009-
{
10101008
#if !NETFRAMEWORK
1011-
request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
1009+
request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), enableWasmStreaming);
10121010
#endif
1013-
}
10141011
}
10151012

10161013
using (var client = new HttpMessageInvoker(CreateHttpClientHandler()))
@@ -1239,7 +1236,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
12391236
// Boolean properties returning correct values
12401237
Assert.True(responseStream.CanRead);
12411238
Assert.False(responseStream.CanWrite);
1242-
Assert.Equal(PlatformDetection.IsBrowser, responseStream.CanSeek);
1239+
Assert.False(responseStream.CanSeek);
12431240

12441241
// Not supported operations
12451242
Assert.Throws<NotSupportedException>(() => responseStream.BeginWrite(new byte[1], 0, 1, null, null));
@@ -1270,11 +1267,14 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
12701267
Assert.Throws<ArgumentOutOfRangeException>(() => { responseStream.CopyToAsync(Stream.Null, -1, default); });
12711268
Assert.Throws<NotSupportedException>(() => { responseStream.CopyToAsync(nonWritableStream, 100, default); });
12721269
Assert.Throws<ObjectDisposedException>(() => { responseStream.CopyToAsync(disposedStream, 100, default); });
1273-
Assert.Throws<ArgumentNullException>(() => responseStream.Read(null, 0, 100));
1274-
Assert.Throws<ArgumentOutOfRangeException>(() => responseStream.Read(new byte[1], -1, 1));
1275-
Assert.ThrowsAny<ArgumentException>(() => responseStream.Read(new byte[1], 2, 1));
1276-
Assert.Throws<ArgumentOutOfRangeException>(() => responseStream.Read(new byte[1], 0, -1));
1277-
Assert.ThrowsAny<ArgumentException>(() => responseStream.Read(new byte[1], 0, 2));
1270+
if (PlatformDetection.IsNotBrowser)
1271+
{
1272+
Assert.Throws<ArgumentNullException>(() => responseStream.Read(null, 0, 100));
1273+
Assert.Throws<ArgumentOutOfRangeException>(() => responseStream.Read(new byte[1], -1, 1));
1274+
Assert.ThrowsAny<ArgumentException>(() => responseStream.Read(new byte[1], 2, 1));
1275+
Assert.Throws<ArgumentOutOfRangeException>(() => responseStream.Read(new byte[1], 0, -1));
1276+
Assert.ThrowsAny<ArgumentException>(() => responseStream.Read(new byte[1], 0, 2));
1277+
}
12781278
Assert.Throws<ArgumentNullException>(() => responseStream.BeginRead(null, 0, 100, null, null));
12791279
Assert.Throws<ArgumentOutOfRangeException>(() => responseStream.BeginRead(new byte[1], -1, 1, null, null));
12801280
Assert.ThrowsAny<ArgumentException>(() => responseStream.BeginRead(new byte[1], 2, 1, null, null));
@@ -1284,29 +1284,37 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
12841284
Assert.Throws<ArgumentNullException>(() => { responseStream.CopyTo(null); });
12851285
Assert.Throws<ArgumentNullException>(() => { responseStream.CopyToAsync(null, 100, default); });
12861286
Assert.Throws<ArgumentNullException>(() => { responseStream.CopyToAsync(null, 100, default); });
1287-
Assert.Throws<ArgumentNullException>(() => { responseStream.Read(null, 0, 100); });
12881287
Assert.Throws<ArgumentNullException>(() => { responseStream.ReadAsync(null, 0, 100, default); });
12891288
Assert.Throws<ArgumentNullException>(() => { responseStream.BeginRead(null, 0, 100, null, null); });
12901289

12911290
// Empty reads
12921291
var buffer = new byte[1];
1293-
Assert.Equal(-1, responseStream.ReadByte());
1294-
Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null));
1292+
if (PlatformDetection.IsNotBrowser)
1293+
{
1294+
Assert.Equal(-1, responseStream.ReadByte());
1295+
Assert.Equal(0, await Task.Factory.FromAsync(responseStream.BeginRead, responseStream.EndRead, buffer, 0, 1, null));
1296+
}
12951297
#if !NETFRAMEWORK
12961298
Assert.Equal(0, await responseStream.ReadAsync(new Memory<byte>(buffer)));
12971299
#endif
12981300
Assert.Equal(0, await responseStream.ReadAsync(buffer, 0, 1));
1301+
if (PlatformDetection.IsNotBrowser)
1302+
{
12991303
#if !NETFRAMEWORK
1300-
Assert.Equal(0, responseStream.Read(new Span<byte>(buffer)));
1304+
Assert.Equal(0, responseStream.Read(new Span<byte>(buffer)));
13011305
#endif
1302-
Assert.Equal(0, responseStream.Read(buffer, 0, 1));
1306+
Assert.Equal(0, responseStream.Read(buffer, 0, 1));
1307+
}
13031308

13041309
// Empty copies
13051310
var ms = new MemoryStream();
13061311
await responseStream.CopyToAsync(ms);
13071312
Assert.Equal(0, ms.Length);
1308-
responseStream.CopyTo(ms);
1309-
Assert.Equal(0, ms.Length);
1313+
if (PlatformDetection.IsNotBrowser)
1314+
{
1315+
responseStream.CopyTo(ms);
1316+
Assert.Equal(0, ms.Length);
1317+
}
13101318
}
13111319
}
13121320
},
@@ -1322,9 +1330,6 @@ public async Task ReadAsStreamAsync_StreamingCancellation()
13221330
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
13231331
{
13241332
var request = new HttpRequestMessage(HttpMethod.Get, uri) { Version = UseVersion };
1325-
#if !NETFRAMEWORK
1326-
request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
1327-
#endif
13281333

13291334
var cts = new CancellationTokenSource();
13301335
using (var client = new HttpMessageInvoker(CreateHttpClientHandler()))

‎src/libraries/Common/tests/System/Net/Http/ResponseStreamTest.cs

+10-11
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public static IEnumerable<object[]> RemoteServersAndReadModes()
2525
{
2626
for (int i = 0; i < 8; i++)
2727
{
28+
if (PlatformDetection.IsBrowser && i is 0 or 2 or 4 or 5 or 6) continue; // ignore sync reads
29+
2830
yield return new object[] { remoteServer, i };
2931
}
3032
}
@@ -176,10 +178,13 @@ public async Task GetStreamAsync_ReadZeroBytes_Success(Configuration.Http.Remote
176178
using (HttpClient client = CreateHttpClientForRemoteServer(remoteServer))
177179
using (Stream stream = await client.GetStreamAsync(remoteServer.EchoUri))
178180
{
179-
Assert.Equal(0, stream.Read(new byte[1], 0, 0));
181+
if (PlatformDetection.IsNotBrowser)
182+
{
183+
Assert.Equal(0, stream.Read(new byte[1], 0, 0));
180184
#if !NETFRAMEWORK
181-
Assert.Equal(0, stream.Read(new Span<byte>(new byte[1], 0, 0)));
185+
Assert.Equal(0, stream.Read(new Span<byte>(new byte[1], 0, 0)));
182186
#endif
187+
}
183188
Assert.Equal(0, await stream.ReadAsync(new byte[1], 0, 0));
184189
}
185190
}
@@ -200,7 +205,7 @@ await client.GetAsync(remoteServer.EchoUri, HttpCompletionOption.ResponseHeaders
200205
cts.Cancel();
201206

202207
// Verify that the task completed.
203-
Assert.True(((IAsyncResult)task).AsyncWaitHandle.WaitOne(new TimeSpan(0, 5, 0)));
208+
Assert.Same(task, await Task.WhenAny(task, Task.Delay(TimeSpan.FromMinutes(5))));
204209
Assert.True(task.IsCompleted, "Task was not yet completed");
205210

206211
// Verify that the task completed successfully or is canceled.
@@ -327,12 +332,10 @@ public async Task BrowserHttpHandler_StreamingResponseAbort(HttpMethod method, s
327332
public async Task BrowserHttpHandler_Streaming()
328333
{
329334
var WebAssemblyEnableStreamingRequestKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingRequest");
330-
var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
331335

332336
var req = new HttpRequestMessage(HttpMethod.Post, Configuration.Http.RemoteHttp2Server.BaseUri + "echobody.ashx");
333337

334338
req.Options.Set(WebAssemblyEnableStreamingRequestKey, true);
335-
req.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
336339

337340
byte[] body = new byte[1024 * 1024];
338341
Random.Shared.NextBytes(body);
@@ -578,16 +581,12 @@ public async Task BrowserHttpHandler_StreamingRequest_Http1Fails()
578581
}
579582

580583
[OuterLoop]
581-
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsChromium))]
584+
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsBrowser))]
582585
public async Task BrowserHttpHandler_StreamingResponseLarge()
583586
{
584-
var WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
585-
586587
var size = 1500 * 1024 * 1024;
587588
var req = new HttpRequestMessage(HttpMethod.Get, Configuration.Http.RemoteSecureHttp11Server.BaseUri + "large.ashx?size=" + size);
588589

589-
req.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
590-
591590
using (HttpClient client = CreateHttpClientForRemoteServer(Configuration.Http.RemoteSecureHttp11Server))
592591
// we need to switch off Response buffering of default ResponseContentRead option
593592
using (HttpResponseMessage response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead))
@@ -605,7 +604,7 @@ public async Task BrowserHttpHandler_StreamingResponseLarge()
605604
int fetchedCount = 0;
606605
do
607606
{
608-
// with WebAssemblyEnableStreamingResponse option set, we will be using https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read
607+
// we will be using https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read
609608
fetchedCount = await stream.ReadAsync(buffer, 0, buffer.Length);
610609
totalCount += fetchedCount;
611610
} while (fetchedCount != 0);

‎src/libraries/System.Net.Http/src/ILLink/ILLink.Substitutions.xml

+5
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@
44
<method signature="System.Boolean IsGloballyEnabled()" body="stub" value="false" feature="System.Net.Http.EnableActivityPropagation" featurevalue="false" />
55
</type>
66
</assembly>
7+
<assembly fullname="System.Net.Http" feature="System.Net.Http.WebAssemblyEnableStreamingResponse" featurevalue="false">
8+
<type fullname="System.Net.Http.BrowserHttpController">
9+
<method signature="System.Boolean get_FeatureEnableStreamingResponse()" body="stub" value="false" />
10+
</type>
11+
</assembly>
712
</linker>

‎src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs

+36-8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Runtime.InteropServices.JavaScript;
1010
using System.Threading;
1111
using System.Threading.Tasks;
12+
using System.Diagnostics.CodeAnalysis;
1213

1314
namespace System.Net.Http
1415
{
@@ -134,6 +135,9 @@ internal sealed class BrowserHttpController : IDisposable
134135
private static readonly HttpRequestOptionsKey<bool> EnableStreamingResponse = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
135136
private static readonly HttpRequestOptionsKey<IDictionary<string, object>> FetchOptions = new HttpRequestOptionsKey<IDictionary<string, object>>("WebAssemblyFetchOptions");
136137

138+
[FeatureSwitchDefinition("System.Net.Http.WasmEnableStreamingResponse")]
139+
internal static bool FeatureEnableStreamingResponse { get; } = AppContextConfigHelper.GetBooleanConfig("System.Net.Http.WasmEnableStreamingResponse", "DOTNET_WASM_ENABLE_STREAMING_RESPONSE", defaultValue: true);
140+
137141
internal readonly JSObject _jsController;
138142
private readonly CancellationTokenRegistration _abortRegistration;
139143
private readonly string[] _optionNames;
@@ -143,7 +147,7 @@ internal sealed class BrowserHttpController : IDisposable
143147
private readonly string uri;
144148
private readonly CancellationToken _cancellationToken;
145149
private readonly HttpRequestMessage _request;
146-
private bool _isDisposed;
150+
internal bool _isDisposed;
147151

148152
public BrowserHttpController(HttpRequestMessage request, bool? allowAutoRedirect, CancellationToken cancellationToken)
149153
{
@@ -314,10 +318,14 @@ private HttpResponseMessage ConvertResponse()
314318
responseMessage.SetReasonPhraseWithoutValidation(responseType);
315319
}
316320

317-
bool streamingResponseEnabled = false;
318-
if (BrowserHttpInterop.SupportsStreamingResponse())
321+
bool streamingResponseEnabled = FeatureEnableStreamingResponse;
322+
if (_request.Options.TryGetValue(EnableStreamingResponse, out var reqStreamingResponseEnabled))
323+
{
324+
streamingResponseEnabled = reqStreamingResponseEnabled;
325+
}
326+
if (streamingResponseEnabled && !BrowserHttpInterop.SupportsStreamingResponse())
319327
{
320-
_request.Options.TryGetValue(EnableStreamingResponse, out streamingResponseEnabled);
328+
throw new PlatformNotSupportedException("Streaming response is not supported in this browser.");
321329
}
322330

323331
responseMessage.Content = streamingResponseEnabled
@@ -361,10 +369,10 @@ public void Dispose()
361369
internal sealed class BrowserHttpWriteStream : Stream
362370
{
363371
private readonly BrowserHttpController _controller; // we don't own it, we don't dispose it from here
372+
364373
public BrowserHttpWriteStream(BrowserHttpController controller)
365374
{
366375
ArgumentNullException.ThrowIfNull(controller);
367-
368376
_controller = controller;
369377
}
370378

@@ -392,7 +400,7 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati
392400

393401
public override bool CanRead => false;
394402
public override bool CanSeek => false;
395-
public override bool CanWrite => true;
403+
public override bool CanWrite => !_controller._isDisposed;
396404

397405
protected override void Dispose(bool disposing)
398406
{
@@ -506,7 +514,7 @@ protected override void Dispose(bool disposing)
506514

507515
internal sealed class BrowserHttpReadStream : Stream
508516
{
509-
private BrowserHttpController _controller; // we own the object and have to dispose it
517+
private readonly BrowserHttpController _controller; // we own the object and have to dispose it
510518

511519
public BrowserHttpReadStream(BrowserHttpController controller)
512520
{
@@ -540,7 +548,7 @@ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancel
540548
return ReadAsync(new Memory<byte>(buffer, offset, count), cancellationToken).AsTask();
541549
}
542550

543-
public override bool CanRead => true;
551+
public override bool CanRead => !_controller._isDisposed;
544552
public override bool CanSeek => false;
545553
public override bool CanWrite => false;
546554

@@ -582,4 +590,24 @@ public override void Write(byte[] buffer, int offset, int count)
582590
}
583591
#endregion
584592
}
593+
594+
internal static class AppContextConfigHelper
595+
{
596+
internal static bool GetBooleanConfig(string switchName, string envVariable, bool defaultValue = false)
597+
{
598+
string? str = Environment.GetEnvironmentVariable(envVariable);
599+
if (str != null)
600+
{
601+
if (str == "1" || str.Equals("true", StringComparison.OrdinalIgnoreCase))
602+
{
603+
return true;
604+
}
605+
if (str == "0" || str.Equals("false", StringComparison.OrdinalIgnoreCase))
606+
{
607+
return false;
608+
}
609+
}
610+
return AppContext.TryGetSwitch(switchName, out bool value) ? value : defaultValue;
611+
}
612+
}
585613
}

‎src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs

+17-2
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,26 @@ namespace System.Net.Http
1111
{
1212
internal static partial class BrowserHttpInterop
1313
{
14+
private static bool? _SupportsStreamingRequest;
15+
private static bool? _SupportsStreamingResponse;
16+
17+
public static bool SupportsStreamingRequest()
18+
{
19+
_SupportsStreamingRequest ??= SupportsStreamingRequestImpl();
20+
return _SupportsStreamingRequest.Value;
21+
}
22+
23+
public static bool SupportsStreamingResponse()
24+
{
25+
_SupportsStreamingResponse ??= SupportsStreamingResponseImpl();
26+
return _SupportsStreamingResponse.Value;
27+
}
28+
1429
[JSImport("INTERNAL.http_wasm_supports_streaming_request")]
15-
public static partial bool SupportsStreamingRequest();
30+
public static partial bool SupportsStreamingRequestImpl();
1631

1732
[JSImport("INTERNAL.http_wasm_supports_streaming_response")]
18-
public static partial bool SupportsStreamingResponse();
33+
public static partial bool SupportsStreamingResponseImpl();
1934

2035
[JSImport("INTERNAL.http_wasm_create_controller")]
2136
public static partial JSObject CreateController();

‎src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System.Runtime.InteropServices.JavaScript.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<!-- This test library intentionally references inbox P2Ps as it needs the implementation, instead of the contract.
2121
Suppress the NU1511 warning in the whole project as putting it on a P2P doesn't work: https://github.com/NuGet/Home/issues/14121 -->
2222
<NoWarn>$(NoWarn);NU1511</NoWarn>
23+
<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
2324
</PropertyGroup>
2425

2526
<!-- Make debugging easier -->

‎src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/HttpRequestMessageTest.cs

+27
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ namespace System.Runtime.InteropServices.JavaScript.Http.Tests
1616
{
1717
public class HttpRequestMessageTest
1818
{
19+
public static readonly string LocalHttpEcho = "http://" + Environment.GetEnvironmentVariable("DOTNET_TEST_HTTPHOST") + "/Echo.ashx";
20+
1921
private readonly Version _expectedRequestMessageVersion = HttpVersion.Version11;
2022
private HttpRequestOptionsKey<bool> EnableStreamingResponse = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
2123
private HttpRequestOptionsKey<IDictionary<string, object?>> FetchOptions = new HttpRequestOptionsKey<IDictionary<string, object?>>("WebAssemblyFetchOptions");
@@ -295,6 +297,31 @@ public void Properties_SetOptionsAndGetTheirValue_NotSet_EnableStreamingResponse
295297
Assert.False(streamingEnabledValue);
296298
}
297299

300+
[Fact]
301+
public async Task HttpStreamingDisabledBy_WasmEnableStreamingResponse_InProject()
302+
{
303+
using var client = new HttpClient();
304+
using var req = new HttpRequestMessage(HttpMethod.Get, LocalHttpEcho + "?guid=" + Guid.NewGuid());
305+
using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
306+
Assert.Equal("BrowserHttpContent", response.Content.GetType().Name);
307+
using var stream = await response.Content.ReadAsStreamAsync();
308+
Assert.Equal("MemoryStream", stream.GetType().Name);
309+
Assert.True(stream.CanSeek);
310+
}
311+
312+
[Fact]
313+
public async Task HttpStreamingEnabledBy_WebAssemblyEnableStreamingResponse_Option()
314+
{
315+
using var client = new HttpClient();
316+
using var req = new HttpRequestMessage(HttpMethod.Get, LocalHttpEcho + "?guid=" + Guid.NewGuid());
317+
req.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
318+
using var response = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
319+
Assert.Equal("StreamContent", response.Content.GetType().Name);
320+
using var stream = await response.Content.ReadAsStreamAsync();
321+
Assert.Equal("ReadOnlyStream", stream.GetType().Name);
322+
Assert.False(stream.CanSeek);
323+
}
324+
298325
[Fact]
299326
public void Version_SetToNull_ThrowsArgumentNullException()
300327
{

‎src/libraries/System.Runtime.InteropServices.JavaScript/tests/System.Runtime.InteropServices.JavaScript.UnitTests/System/Runtime/InteropServices/JavaScript/WebWorkerTest.Http.cs

-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ await executor.Execute(async () =>
3232
}
3333

3434
private static HttpRequestOptionsKey<bool> WebAssemblyEnableStreamingRequestKey = new("WebAssemblyEnableStreamingRequest");
35-
private static HttpRequestOptionsKey<bool> WebAssemblyEnableStreamingResponseKey = new("WebAssemblyEnableStreamingResponse");
3635
private static string HelloJson = "{'hello':'world'}".Replace('\'', '"');
3736
private static string EchoStart = "{\"Method\":\"POST\",\"Url\":\"/Echo.ashx";
3837

@@ -46,7 +45,6 @@ private async Task HttpClient_ActionInDifferentThread(string url, Executor execu
4645
await ms.WriteAsync(Encoding.UTF8.GetBytes(HelloJson));
4746

4847
using var req = new HttpRequestMessage(HttpMethod.Post, url);
49-
req.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
5048
req.Content = new StreamContent(ms);
5149
using var client = new HttpClient();
5250
var pr = client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);

‎src/mono/browser/browser.proj

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<ICULibDir Condition="'$(WasmEnableThreads)' != 'true'">$([MSBuild]::NormalizeDirectory('$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)', 'runtimes', 'browser-wasm', 'native', 'lib'))</ICULibDir>
2020
<ICULibDir Condition="'$(WasmEnableThreads)' == 'true'">$([MSBuild]::NormalizeDirectory('$(PkgMicrosoft_NETCore_Runtime_ICU_Transport)', 'runtimes', 'browser-wasm-threads', 'native', 'lib'))</ICULibDir>
2121
<InvariantTimezone Condition="'$(InvariantTimezone)' == ''">false</InvariantTimezone>
22+
<WasmEnableStreamingResponse Condition="'$(WasmEnableStreamingResponse)' == ''">true</WasmEnableStreamingResponse>
2223
<InvariantGlobalization Condition="'$(InvariantGlobalization)' == ''">false</InvariantGlobalization>
2324
<WasmNativeStrip Condition="'$(WasmNativeStrip)' == ''">true</WasmNativeStrip>
2425
<WasmSingleFileBundle Condition="'$(WasmSingleFileBundle)' == ''">false</WasmSingleFileBundle>
@@ -335,6 +336,7 @@
335336
"PropertiesThatTriggerRelinking": [
336337
{ "identity": "InvariantTimezone", "defaultValueInRuntimePack": "$(InvariantTimezone)" },
337338
{ "identity": "InvariantGlobalization", "defaultValueInRuntimePack": "$(InvariantGlobalization)" },
339+
{ "identity": "WasmEnableStreamingResponse", "defaultValueInRuntimePack": "$(WasmEnableStreamingResponse)" },
338340
{ "identity": "WasmNativeStrip", "defaultValueInRuntimePack": "$(WasmNativeStrip)" },
339341
{ "identity": "WasmSingleFileBundle", "defaultValueInRuntimePack": "$(WasmSingleFileBundle)" },
340342
{ "identity": "WasmEnableSIMD", "defaultValueInRuntimePack": "$(WasmEnableSIMD)" },

‎src/mono/browser/build/BrowserWasmApp.targets

+10
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,18 @@
5959
<_WasmDefaultFlags Condition="'$(WasmEnableSIMD)' == 'true'">-msimd128</_WasmDefaultFlags>
6060

6161
<_WasmOutputFileName Condition="'$(WasmSingleFileBundle)' != 'true'">dotnet.native.wasm</_WasmOutputFileName>
62+
63+
<WasmEnableStreamingResponse Condition="'$(WasmEnableStreamingResponse)' == ''">true</WasmEnableStreamingResponse>
6264
</PropertyGroup>
6365

66+
67+
<ItemGroup>
68+
<RuntimeHostConfigurationOption Include="System.Net.Http.WasmEnableStreamingResponse"
69+
Condition="'$(WasmEnableStreamingResponse)' != ''"
70+
Value="$(WasmEnableStreamingResponse)"
71+
Trim="true" />
72+
</ItemGroup>
73+
6474
<ItemGroup>
6575
<!-- Allow running/debugging from VS -->
6676
<ProjectCapability Include="DotNetCoreWeb"/>

‎src/mono/browser/runtime/http.ts

+3-14
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ function commonAsserts (controller: HttpController) {
2626
mono_assert(controller, "expected controller");
2727
}
2828

29-
let http_wasm_supports_streaming_request_cached: boolean | undefined;
3029
export function http_wasm_supports_streaming_request (): boolean {
31-
if (http_wasm_supports_streaming_request_cached !== undefined) {
32-
return http_wasm_supports_streaming_request_cached;
33-
}
3430
// Detecting streaming request support works like this:
3531
// If the browser doesn't support a particular body type, it calls toString() on the object and uses the result as the body.
3632
// 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 {
4844
return "half";
4945
},
5046
} as RequestInit /* https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1483 */).headers.has("Content-Type");
51-
http_wasm_supports_streaming_request_cached = duplexAccessed && !hasContentType;
52-
} else {
53-
http_wasm_supports_streaming_request_cached = false;
47+
return duplexAccessed && !hasContentType;
5448
}
55-
return http_wasm_supports_streaming_request_cached;
49+
return false;
5650
}
5751

58-
let http_wasm_supports_streaming_response_cached: boolean | undefined;
5952
export function http_wasm_supports_streaming_response (): boolean {
60-
if (http_wasm_supports_streaming_response_cached !== undefined) {
61-
return http_wasm_supports_streaming_response_cached;
62-
}
63-
http_wasm_supports_streaming_response_cached = typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function";
64-
return http_wasm_supports_streaming_response_cached;
53+
return typeof Response !== "undefined" && "body" in Response.prototype && typeof ReadableStream === "function";
6554
}
6655

6756
export function http_wasm_create_controller (): HttpController {

‎src/mono/nuget/Microsoft.NET.Workload.Mono.Toolchain.Current.Manifest/WasmFeatures.props

+1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
<EnableUnsafeUTF7Encoding Condition="'$(EnableUnsafeUTF7Encoding)' == ''">false</EnableUnsafeUTF7Encoding>
99
<HttpActivityPropagationSupport Condition="'$(HttpActivityPropagationSupport)' == ''">false</HttpActivityPropagationSupport>
1010
<DebuggerSupport Condition="'$(DebuggerSupport)' == '' and '$(Configuration)' != 'Debug'">false</DebuggerSupport>
11+
<WasmEnableStreamingResponse Condition="'$(WasmEnableStreamingResponse)' == ''">true</WasmEnableStreamingResponse>
1112
</PropertyGroup>
1213
</Project>

‎src/mono/wasm/features.md

+8
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ Because web browsers do not expose direct access to sockets, we are unable to pr
5555

5656
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.
5757

58+
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.
59+
60+
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<Byte>)`. If your code uses synchronous operations, you can disable the feature or copy the stream into `MemoryStream` yourself.
61+
62+
If you need to disable it, you can use `<WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>` or `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` env variable to do it for all HTTP requests.
63+
64+
Or you can use `request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), false);` for individual request.
65+
5866
### WebSocket
5967
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.
6068

0 commit comments

Comments
 (0)
Please sign in to comment.