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

BrowserHttpHandler can't stream large responses even with SetBrowserResponseStreamingEnabled #60287

Closed
SteveSandersonMS opened this issue Oct 12, 2021 · 9 comments · Fixed by #60339
Assignees
Labels
arch-wasm WebAssembly architecture area-System.Net.Http
Milestone

Comments

@SteveSandersonMS
Copy link
Member

SteveSandersonMS commented Oct 12, 2021

Description

On WebAssembly, HttpClient.SendAsync can't handle large responses (e.g., > 1GB) and fails with errors like

Uncaught (in promise) ExitStatus {name: 'ExitStatus', message: 'Program terminated with exit(0)', status: 0}

while also logging an absolutely giant stack trace (many screens high, dozens or maybe hundreds) starting with stuff like this:

580530 | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | _emscripten_asm_const_int | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | $func5907 | @ | 00945abe:0x14ccd8
  | $func8601 | @ | 00945abe:0x1b46be
  | $func3936 | @ | 00945abe:0xd198a
  | $func1127 | @ | 00945abe:0x29bd1
  | $func106 | @ | 00945abe:0x515c
  | $func2788 | @ | 00945abe:0x8fb78
  | $func119 | @ | 00945abe:0x56b4
  | $func159 | @ | 00945abe:0x6120
  | $func878 | @ | 00945abe:0x1dc5f
  | $func181 | @ | 00945abe:0x6403
  | $func2927 | @ | 00945abe:0x94e01
  | $func761 | @ | 00945abe:0x1952b
  | $func163 | @ | 00945abe:0x6181
  | $func2307 | @ | 00945abe:0x6fdca
  | $func7015 | @ | 00945abe:0x17bac4
  | $func6830 | @ | 00945abe:0x172bc5
  | $func3524 | @ | 00945abe:0xb47cc
  | $func675 | @ | 00945abe:0x16602
  | $fb | @ | 00945abe:0x16f60e
  | Module._mono_wasm_invoke_method | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | managed_BINDINGS_SetTaskSourceResult | @ | managed_BINDINGS_SetTaskSourceResult:17
  | (anonymous) | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | Promise.then (async) |   |  
  | _wrap_js_thenable_as_task | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | _js_to_mono_obj | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | _mono_wasm_invoke_js_with_args | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | $func2588 | @ | 00945abe:0x80614
  | $func2307 | @ | 00945abe:0x689a7
  | $func7015 | @ | 00945abe:0x17bac4
  | $func6830 | @ | 00945abe:0x172bc5
  | $func3524 | @ | 00945abe:0xb47cc
  | $func675 | @ | 00945abe:0x16602
  | $fb | @ | 00945abe:0x16f60e
  | Module._mono_wasm_invoke_method | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | managed_BINDINGS_SetTaskSourceResult | @ | managed_BINDINGS_SetTaskSourceResult:17
  | (anonymous) | @ | dotnet.6.0.0-rtm.21508.5.38wx3tlowm.js:1
  | Promise.then (async) | 

This was originally reported at dotnet/aspnetcore#36872

Reproduction Steps

Using code like the following on WebAssembly:

var req = new HttpRequestMessage(HttpMethod.Get, someUrl));	
req.SetBrowserResponseStreamingEnabled(true);
var response = await HttpClient.SendAsync(req); // Crashes here with big files
Console.WriteLine(response.Content.Headers.ContentLength);

... it's possible to get a Stream representing the body of an HTTP response, as long as it's pretty small. For larger responses (tested with 1 GB), it fails with the above error.

Note that I also tried without SetBrowserResponseStreamingEnabled, and it also crashed, but with the same error (Uncaught (in promise) ExitStatus {name: 'ExitStatus', message: 'Program terminated with exit(0)', status: 0} but without the giant stack trace.

Expected behavior

Should be possible to read from arbitrary-sized response streams, assuming the developer doesn't choose to read it all into memory at once.

Actual behavior

Crashes as described above

Regression?

Don't know

Known Workarounds

None

Configuration

Blazor WebAssembly on SDK 6.0.100-rtm.21508.19, any browser

Other information

Based on real customer issue: dotnet/aspnetcore#36872

@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Net.Http untriaged New issue has not been triaged by the area owner labels Oct 12, 2021
@ghost
Copy link

ghost commented Oct 12, 2021

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

On WebAssembly, HttpClient.GetAsync can't handle large responses (e.g., > 1GB) and fails with errors like

Uncaught (in promise) ExitStatus {name: 'ExitStatus', message: 'Program terminated with exit(0)', status: 0}

or

Uncaught RuntimeError: memory access out of bounds

This was originally reported at dotnet/aspnetcore#36872

Reproduction Steps

Using code like the following on WebAssembly:

using var response = await HttpClient.GetAsync(someUrl));
using var stream = response.Content.ReadAsStream();

... it's possible to get a Stream representing the body of an HTTP response, as long as it's pretty small. For larger responses (tested with 1 GB), it fails with the above errors.

I think the underlying problem is this code:

protected override async Task<Stream> CreateContentReadStreamAsync()
{
byte[] data = await GetResponseData(CancellationToken.None).ConfigureAwait(continueOnCapturedContext: true);
return new MemoryStream(data, writable: false);
}

Instead of making an actual stream backed by the JS-side Response object, it's trying to read the entire response into .NET memory as a single array. That can't scale indefinitely.

Expected behavior

Should be possible to read from arbitrary-sized response streams, assuming the developer doesn't choose to read it all into memory at once.

Actual behavior

Crashes with errors like

Uncaught (in promise) ExitStatus {name: 'ExitStatus', message: 'Program terminated with exit(0)', status: 0}

or

Uncaught RuntimeError: memory access out of bounds

Regression?

No

Known Workarounds

None

Configuration

Blazor WebAssembly on SDK 6.0.100-rtm.21508.19, any browser

Other information

Real customer issue: dotnet/aspnetcore#36872

Author: SteveSandersonMS
Assignees: -
Labels:

area-System.Net.Http, untriaged

Milestone: -

@ghost
Copy link

ghost commented Oct 12, 2021

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

Issue Details

Description

On WebAssembly, HttpClient.GetAsync can't handle large responses (e.g., > 1GB) and fails with errors like

Uncaught (in promise) ExitStatus {name: 'ExitStatus', message: 'Program terminated with exit(0)', status: 0}

or

Uncaught RuntimeError: memory access out of bounds

This was originally reported at dotnet/aspnetcore#36872

Reproduction Steps

Using code like the following on WebAssembly:

using var response = await HttpClient.GetAsync(someUrl);
using var stream = response.Content.ReadAsStream();

... it's possible to get a Stream representing the body of an HTTP response, as long as it's pretty small. For larger responses (tested with 1 GB), it fails with the above errors.

I think the underlying problem is this code:

protected override async Task<Stream> CreateContentReadStreamAsync()
{
byte[] data = await GetResponseData(CancellationToken.None).ConfigureAwait(continueOnCapturedContext: true);
return new MemoryStream(data, writable: false);
}

Instead of making an actual stream backed by the JS-side Response object, it's trying to read the entire response into .NET memory as a single array. That can't scale indefinitely.

Expected behavior

Should be possible to read from arbitrary-sized response streams, assuming the developer doesn't choose to read it all into memory at once.

Actual behavior

Crashes with errors like

Uncaught (in promise) ExitStatus {name: 'ExitStatus', message: 'Program terminated with exit(0)', status: 0}

or

Uncaught RuntimeError: memory access out of bounds

Regression?

No

Known Workarounds

None

Configuration

Blazor WebAssembly on SDK 6.0.100-rtm.21508.19, any browser

Other information

Real customer issue: dotnet/aspnetcore#36872

Author: SteveSandersonMS
Assignees: -
Labels:

arch-wasm, area-System.Net.Http, untriaged

Milestone: -

@campersau
Copy link
Contributor

You can enable response streaming by using:
https://github.com/dotnet/aspnetcore/blob/8b30d862de6c9146f466061d51aa3f1414ee2337/src/Components/WebAssembly/WebAssembly/src/Http/WebAssemblyHttpRequestMessageExtensions.cs#L148-L169

And in the BrowserHttpHandler it will check it here:

bool streamingEnabled = false;
if (StreamingSupported)
{
request.Options.TryGetValue(EnableStreamingResponse, out streamingEnabled);
}
httpResponse.Content = streamingEnabled
? new StreamContent(wasmHttpReadStream = new WasmHttpReadStream(status))
: (HttpContent)new BrowserHttpContent(status);

@SteveSandersonMS SteveSandersonMS changed the title BrowserHttpHandler can't stream large responses BrowserHttpHandler can't stream large responses even with SetBrowserResponseStreamingEnabled Oct 12, 2021
@SteveSandersonMS
Copy link
Member Author

@campersau Thanks - I had forgotten about that API. Unfortunately after trying it, it still seems not to work. I've update this issue description to cover that. If you think I'm still doing something wrong please let me know!

@lewing lewing removed the untriaged New issue has not been triaged by the area owner label Oct 12, 2021
@lewing lewing added this to the 7.0.0 milestone Oct 12, 2021
pavelsavara added a commit to pavelsavara/runtime that referenced this issue Oct 13, 2021
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Oct 13, 2021
@pavelsavara
Copy link
Member

pavelsavara commented Oct 13, 2021

In order to be able to use WebAssemblyEnableStreamingResponse/SetBrowserResponseStreamingEnabled option on large files, the user should also set HttpCompletionOption.ResponseHeadersRead so that the content would not be cached in memory. The default is ResponseContentRead which makes HttpClient to "... complete after reading the entire response including the content."

@pavelsavara
Copy link
Member

^^ FYI @ylr-research

@dotnetspark
Copy link

I'll try that

pavelsavara added a commit that referenced this issue Oct 14, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Oct 14, 2021
@dotnetspark
Copy link

@pavelsavara thanks so much for the suggestion. @SteveSandersonMS thanks for following up on this issue. I was able to successfully download the file with both JavaScript and HttpClient.
@SteveSandersonMS for JS function I used the one you recommended -see below- and the response time for 1GB was ~11s.

        async function download(url, fileName) {
            var link = document.createElement('a');
            link.setAttribute('href', url);
            link.setAttribute('download', fileName);
            link.click();
        }

On the other hand, same file using HttpClient was ~39s. Although the JS function is 2x times faster I see HttpClient has shown great improvements. See below the snapshot for reference.

dotnetstream

@dotnetspark
Copy link

This can be closed

@ghost ghost locked as resolved and limited conversation to collaborators Dec 6, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
arch-wasm WebAssembly architecture area-System.Net.Http
Projects
None yet
5 participants