Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[browser] [wasm] Make response streaming opt-out #111680

Merged
merged 9 commits into from
Mar 17, 2025

Conversation

campersau
Copy link
Contributor

@campersau campersau commented Jan 21, 2025

This PR makes browser HTTP client to support streaming HTTP response by default. Until now, this feature was opt-in.

Because all evergreen browsers now support it.

This is compelling because it will make the GetFromJsonAsAsyncEnumerable streaming and lower memory consumption for large requests.

breaking change

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.

how to opt-out

If you need to disable it globally, you can use

  • <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse> in your project file
  • set env variable DOTNET_WASM_ENABLE_STREAMING_RESPONSE

To disable it for individual request, you can use existing

  • request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), false);
  • or request.SetBrowserResponseStreamingEnabled(false) extension in Blazor project

Related to #77904
Fixes to #112442

Copy link
Member

@lewing lewing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support this in principle. I'm concerned about how the breaking change will surface to people upgrading. My assumption is that it at the moment it will only show up at runtime?

@campersau
Copy link
Contributor Author

Yes, it will only show up at runtime. Similar to Kestrels AllowSynchronousIO defaulting to false.
The main issue I will have with dotnet/runtime types is that ZipArchive does not support async #1541 and would throw.
An other workaround is to buffer the stream manually.

Could we add an app context switch to toggle this globally?

@pavelsavara pavelsavara added arch-wasm WebAssembly architecture os-browser Browser variant of arch-wasm labels Jan 22, 2025
Copy link
Contributor

Tagging subscribers to 'arch-wasm': @lewing
See info in area-owners.md if you want to be subscribed.

@pavelsavara
Copy link
Member

pavelsavara commented Feb 4, 2025

Could we add an app context switch to toggle this globally?

We could make it ILLink feature.
Similar to <InvariantGlobalization>true</InvariantGlobalization> for example.

I'm not sure if making the breaking change is worth it. Most people don't stream large data.

I would stick to existing behavior as default. Ok, changed my mind

@MihaZupan
Copy link
Member

FWIW this shouldn't be specific to just streaming large data.
A simple use case this affects can be streaming an AI chat response via GetFromJsonAsAsyncEnumerable or SseParser, where the preferred experience is that a partial response is displayed as soon as available.

@lewing
Copy link
Member

lewing commented Feb 4, 2025

The GetFromJsonAsAsyncEnumerable case is very compelling to me given that the overhead of processing large json blobs is particularly painful in both memory and responsiveness in browser with a single thread.

@campersau
Copy link
Contributor Author

To me it looks like everyone supports this change. I guess it is still early to try it out in the previews then?

@pavelsavara
Copy link
Member

@campersau I think we are missing the global opt-out linker feature. Are you willing to improve the PR ?

@pavelsavara
Copy link
Member

I will work on adding global-opt out soon. @campersau I will take over this branch, ok ?

ZipArchive does not support async

BrowserHttpContent doesn't support sync API already.
So think this would not break anything new. Am I missing something ?

Yes, it will only show up at runtime.

If the developer didn't opt out, I can see few options:

A) at the start of request we can detect old browser and fail the request with some message hinting about possibilities how to opt out.
B) at the start of the runtime we can detect old browser and fail the runtime startup
C) we can detect old browser and resolve promise only once we received full response (no streaming) + issue warning to console

Thoughts ?

@campersau
Copy link
Contributor Author

I will take over this branch, ok ?

Okay, I have tried to figure out how to do the linker opt out, but it is hard to find all the places which need to be updated.

BrowserHttpContent doesn't support sync API already.
So think this would not break anything new. Am I missing something ?

The BrowserHttpContent itself is only async but the Stream returned by BrowserHttpContent.ReadAsStreamAsync supports sync because it is just a MemoryStream:

If the developer didn't opt out, I can see few options:
A) at the start of request we can detect old browser and fail the request with some message hinting about possibilities how to opt out.
B) at the start of the runtime we can detect old browser and fail the runtime startup
C) we can detect old browser and resolve promise only once we received full response (no streaming) + issue warning to console

I think all supported browsers support response streaming so I am not sure how you will detect it.
See #111680 (comment)

The true value here should be replaced by the global opt out value:
https://github.com/dotnet/runtime/pull/111680/files#diff-66ce677ded58c3a6a0639d321f4460bb0b2c4bcaeb3216ff3db4381937365bd8R317
Otherwise the developer can still opt out on a per request basis by setting WebAssemblyEnableStreamingResponse to false.

@pavelsavara
Copy link
Member

Okay, I have tried to figure out how to do the linker opt out, but it is hard to find all the places which need to be updated.

Yes, no worries.

I think all supported browsers support response streaming

Use the same detection we have there already. To detect unsupported browser and proceed or fail in meaningful way.

@pavelsavara
Copy link
Member

FYI @guardrex

@guardrex
Copy link

guardrex commented Mar 5, 2025

@pavelsavara ... Is the new opt-out approach ...

request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), false);

... landing at Preview 2?

- System.Net.Http.WasmEnableStreamingResponse linker feature
- DOTNET_WASM_ENABLE_STREAMING_RESPONSE env variable
- tests & docs
- System.Net.Http.WasmEnableStreamingResponse linker feature
- DOTNET_WASM_ENABLE_STREAMING_RESPONSE env variable
- tests & docs
@pavelsavara
Copy link
Member

... landing at Preview 2?

@guardrex probably Preview 3. I added some text in src/mono/wasm/features.md on this PR.

I also updated description on top of this PR

@guardrex
Copy link

guardrex commented Mar 7, 2025

Thanks @pavelsavara. I'll react to the updated description on the docs PR next week, and I'll ping for docs PR review later.

@pavelsavara
Copy link
Member

This passed both FF and chrome tests.

@pavelsavara
Copy link
Member

@campersau do you have any comments, I think this is ready

@@ -4,4 +4,9 @@
<method signature="System.Boolean IsGloballyEnabled()" body="stub" value="false" feature="System.Net.Http.EnableActivityPropagation" featurevalue="false" />
</type>
</assembly>
<assembly fullname="System.Net.Http" feature="System.Net.Http.WebAssemblyEnableStreamingResponse" featurevalue="false">
<type fullname="System.Net.Http.BrowserHttpController">
<method signature="System.Boolean get_FeatureEnableStreamingResponse()" body="stub" value="false" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is value="false" / featurevalue="false"? The default value should be true or is this something else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means: if you set System.Net.Http.WebAssemblyEnableStreamingResponse to false then ILLink will stub the get_FeatureEnableStreamingResponse to return false instead of reading the config/env.

We don't want to also hardcode true do we ?

Copy link
Member

@javiercn javiercn Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the behavior here the same in development as in publish?

We have something similar in Blazor routing and we had to do extra work for the development case
https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/Properties/ILLink.Substitutions.xml#L6-L8 for routing constraints, and it doesn't work in development unless you specifically set the AppContextSwitch manually

https://github.com/dotnet/aspnetcore/blob/f25dc7be397b496cc1c71f8720b2b7d67fb18649/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs#L98-L112

My understanding is that runtimeConfig.json stuff doesn't flow to wasm at the moment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -11,11 +11,26 @@ namespace System.Net.Http
{
internal static partial class BrowserHttpInterop
{
private static bool? _SupportsStreamingRequest;
private static bool? _SupportsStreamingResponse;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is now cached in .NET we don't need to cache it in JS anymore. Does not hurt though and might still be relevant for MT.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it there, thanks. I believe that the webworker has the same feature set as UI thread.
If not, having static cache in C# side would be wrong.

pavelsavara and others added 2 commits March 11, 2025 09:59
Co-authored-by: campersau <buchholz.bastian@googlemail.com>
@pavelsavara
Copy link
Member

pavelsavara commented Mar 11, 2025

This is now dependent on #97449 because without it the Blazor dev loop would not reflect the AppContext feature switch.

@pavelsavara
Copy link
Member

@lewing I think we should merge it into P3 anyway. Quite likely dev-loop users do have latest browsers.
So they would be only hit by the MemoryStream sync APIs problem and we would hear feedback sooner.

@pavelsavara
Copy link
Member

/ba-g CI fails are unrelated

@pavelsavara pavelsavara merged commit e12a753 into dotnet:main Mar 17, 2025
90 of 94 checks passed
@campersau campersau deleted the responsestreamingoptin branch March 17, 2025 17:03
@pavelsavara
Copy link
Member

Hmm, my bad, I overlooked HttpStreamingDisabledBy_WasmEnableStreamingResponse_InProject fail

#113628

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
arch-wasm WebAssembly architecture area-System.Net.Http community-contribution Indicates that the PR has been added by a community member os-browser Browser variant of arch-wasm
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants