Skip to content

Commit e12a753

Browse files
[browser][wasm] Make response streaming opt-out (#111680)
Co-authored-by: campersau <[email protected]> Co-authored-by: pavelsavara <[email protected]>
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

Lines changed: 0 additions & 5 deletions
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

Lines changed: 25 additions & 20 deletions
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

Lines changed: 10 additions & 11 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 36 additions & 8 deletions
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

Lines changed: 17 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
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 -->

0 commit comments

Comments
 (0)