dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.79k stars 9.83k forks source link

"network error" - unhandled exception - Blazor WASM - Streaming http requests from IAsyncEnumerable api endpoint #55982

Open radderz opened 1 month ago

radderz commented 1 month ago

Is there an existing issue for this?

Describe the bug

When using IAsyncEnumerable response type from an asp.net core API and using WebAssemblyEnableStreamingResponse if there is a network error, an unhandled exception is thrown outside of the caller scope.

i.e. there is no way to catch the error and this ends up displaying a Blazor UI error that cannot be controlled. It doesn't break anything and the site just continues working, but the error shows up in the console and shows an error popup on the UI.

A try catch around this doesn't suppress the error.

try
{
    using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, $"/api/xxxxxxx");

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

    using var httpClient = HttpClientFactory.CreateClient("Platform.Api");
    httpClient.Timeout = TimeSpan.FromHours(2);
    using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);

    response.EnsureSuccessStatusCode();

    using Stream responseStream = await response.Content.ReadAsStreamAsync(token);

    var stateEnumerable = (IAsyncEnumerable<List<AssetState>>)JsonSerializer.DeserializeAsyncEnumerable<List<AssetState>>(
            responseStream,
            _jsonSerializerOptionsStreaming,
            token
        );

    await foreach (List<AssetState> assetStateBatch in stateEnumerable)
    {
    }
}
catch (OperationCanceledException)
{
    // do nothing, cancelled.
}
catch (Exception err)
{
    Console.WriteLine("StreamAssetStates - Exception: {0}", err);
}

The below image shows the error in the console, you can see the console write line in between from the catch, but there is still an unhandled error outside the scope.

image

Expected Behavior

Steps To Reproduce

The steps are a reasonably simple but the blazor UI needs to be connected to an api with the ability to remove connectivity (i.e. pulling out the network cable or disconnect wifi). So the api needs to be on a different machine or be isolatable from the browser running the blazor wasm UI. Possibly devtunnels would make this easy to reproduce.

  1. Run an API with an IAsyncEnumerable endpoint that runs forever
  2. Run client connected to this endpoint on a different host
  3. Disconnect/break the network connection between the browser and the api to trigger a network error

Exceptions (if any)

Error in http_wasm_abort_response: TypeError: network error

.NET Version

8.0.300

Anything else?

No response

radderz commented 3 weeks ago

https://github.com/radderz/BlazorAppJsonStreamBug

You can run the asp.net core project, which is hosting the web assembly page, once you are on the page and the timer is updating, kill the asp.net core api projects process (don't gracefully shut it down, do a kill) and you'll see the unhandled network error.

halter73 commented 3 weeks ago

Thanks for the report. Can this be reproduced without IAsyncEnumerable? Also, when there is no network error, does JsonSerializer.DeserializeAsyncEnumerable<List<AssetState>>(stream) give you access to the array items before the entire array is complete? I would have expected you to need to use IAsyncEnumerable rather than List for the generic parameter.

radderz commented 3 weeks ago

Thanks for the report. Can this be reproduced without IAsyncEnumerable? Also, when there is no network error, does JsonSerializer.DeserializeAsyncEnumerable<List<AssetState>>(stream) give you access to the array items before the entire array is complete? I would have expected you to need to use IAsyncEnumerable rather than List for the generic parameter.

I don't think it is really related to IAsync Enumerable, it's more the streaming http endpoint. It could be a non dotnet api.

I think this is more on the wasm side.

Yes I get the results streaming imediately (which is the whole point of the streaming api) before the network error when the connection fails mid way ungracefully. If I close the connection gracefully on either side this doesn't happen. It's good for streaming down real time changes. I could use a different technology potentially like websockets but this is convenient.

I haven't tested if this happens for a long running standard http get request where the server drops mid request.

radderz commented 3 weeks ago

Sorry I take that back, even gracefully closing the connection from the server side also results in this unhandled error.

radderz commented 2 days ago

Thanks for the report. Can this be reproduced without IAsyncEnumerable? Also, when there is no network error, does JsonSerializer.DeserializeAsyncEnumerable<List<AssetState>>(stream) give you access to the array items before the entire array is complete? I would have expected you to need to use IAsyncEnumerable rather than List for the generic parameter.

Hi @halter73, I forgot to answer the generic part, the response I am sending is an IAsyncEnumerable<List> as it batches changes. it doesn't have to be a list and the sample I provided is just a simple DateTime.