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

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

Closed
1 task done
radderz opened this issue May 30, 2024 · 17 comments
Assignees
Labels
area-blazor Includes: Blazor, Razor Components
Milestone

Comments

@radderz
Copy link

radderz commented May 30, 2024

Is there an existing issue for this?

  • I have searched the existing issues

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

  • No unhandled exception outside of the scope of the caller, so that the callers Try Catch statements can handle the error.
  • No Blazor Error UI popup

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

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-blazor Includes: Blazor, Razor Components label May 30, 2024
@javiercn javiercn added Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. Needs: Repro Indicates that the team needs a repro project to continue the investigation on this issue labels Jun 2, 2024
@radderz
Copy link
Author

radderz commented Jun 7, 2024

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.

@dotnet-policy-service dotnet-policy-service bot added Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. and removed Needs: Author Feedback The author of this issue needs to respond in order for us to continue investigating this issue. Status: No Recent Activity labels Jun 7, 2024
@halter73 halter73 removed Needs: Repro Indicates that the team needs a repro project to continue the investigation on this issue Needs: Attention 👋 This issue needs the attention of a contributor, typically because the OP has provided an update. labels Jun 11, 2024
@halter73 halter73 added this to the Backlog milestone Jun 11, 2024
@halter73
Copy link
Member

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
Copy link
Author

radderz commented Jun 11, 2024

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
Copy link
Author

radderz commented Jun 12, 2024

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

@radderz
Copy link
Author

radderz commented Jul 1, 2024

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.

@aomader
Copy link

aomader commented Sep 16, 2024

I second this. We see similar behavior with streaming responses via gRPC-Web. It is not possible to catch those connection-related issues and prevent an error popup.

@radderz
Copy link
Author

radderz commented Jan 30, 2025

I have just had this same issue with trying to implement streaming responses with gRPC-Web too, it looks like anything that is streaming is being handled in javascript as part of the HttpClient, and the error on intermittent connection failure doesn't bubble up to the C# function that called it and instead becomes an unhandled exception.

All you need to do is start a stream and have it fail midway or just not work at all and it will cause an unhandled exception.

Image

However one thing I see is this looks to me like something to do with visual studio instrumentation or something, I'll have to try a published build with no instrumentation to make sure this doesn't happen in production.

@radderz
Copy link
Author

radderz commented Jan 30, 2025

Image

nevermind this definitely isn't just the instrumentation hooks, try catches around these streaming calls that use HttpClient in wasm are just not bubbling the error to the right place.

@drungrin
Copy link

drungrin commented Feb 5, 2025

I'm also running into this exact behavior with Blazor WebAssembly. It appears that when streaming (whether via gRPC-Web or an IAsyncEnumerable HTTP response), a disconnection or network error triggers http_wasm_abort in the underlying JavaScript/WASM layer, but this error never bubbles up correctly into Blazor’s C# exception-handling flow.

Even if I wrap the streaming call in a try/catch, the network error is reported as an unhandled exception outside of Blazor’s control. This causes Blazor to show the generic error dialog instead of letting my application handle it gracefully.

It looks like this might be a limitation in how http_wasm_abort is surfaced in Blazor WebAssembly, preventing it from being caught in normal .NET control flow. If there’s any workaround or plan to fix the behavior so Blazor can handle network aborts more gracefully, that would be really helpful.

@radderz
Copy link
Author

radderz commented Feb 5, 2025

I don't think its related to GRPC at all other than when streaming with GRPC it uses the HttpClient to stream since its grpc-web.

I think it is the HttpClient interop when having a streaming result. It only occurs when the stream is up and then a connection failure occurs. I think it happens with streaming because I might work by returning a reference to the stream and processing through that as changes come so the control flow is a bit different to a normal request. I am not really sure how that part in the HttpClient works in wasm blazor.

@drungrin
Copy link

drungrin commented Feb 5, 2025

I don't think its related to GRPC at all other than when streaming with GRPC it uses the HttpClient to stream since its grpc-web.

I think it is the HttpClient interop when having a streaming result. It only occurs when the stream is up and then a connection failure occurs. I think it happens with streaming because I might work by returning a reference to the stream and processing through that as changes come so the control flow is a bit different to a normal request. I am not really sure how that part in the HttpClient works in wasm blazor.

It is related to streaming and abort.

Http implementation is there, on http_wasm_abort and handle_abort_error

https://github.com/dotnet/runtime/blob/main/src/mono/browser/runtime/http.ts
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpInterop.cs

I'm trying to circunvent it, but still nothing.

@radderz
Copy link
Author

radderz commented Feb 5, 2025

It looks to me that

function handle_abort_error (promise:Promise<any>) {
    promise.catch((err) => {
        if (err && err !== "AbortError" && err.name !== "AbortError" ) {
            Module.err("Unexpected error: " + err);
        }
        // otherwise, it's expected
    });
}

Should be changed to

function handle_abort_error (promise:Promise<any>) {
    promise.catch((err) => {
        if (!err) 
               return;        
        if (err === "AbortError" || err.name == "AbortError") 
                return; // normal abort
        if (err === "TypeError: network error" || (err.name == "TypeError" && err.message == "network error")) 
                return; // stream connection failure, should be handled by application

       Module.err("Unexpected error: " + err);
    });
}

Seems like a simple change.

Image

Image

@radderz
Copy link
Author

radderz commented Feb 5, 2025

I am just not sure this actually fixes it as I am unsure how to debug this

@radderz
Copy link
Author

radderz commented Feb 5, 2025

@halter73 does the above make sense?

@radderz
Copy link
Author

radderz commented Feb 5, 2025

My snippets in browser is the .net8 version, the dotnet9 version hence the slightly different naming of the functions and different message constructed when an unhandled exception is sent to Module.err.

@pavelsavara
Copy link
Member

Should we close this in favor of dotnet/runtime#112172 ?

@radderz radderz closed this as completed Feb 20, 2025
@pavelsavara
Copy link
Member

I'm working on fix in dotnet/runtime#113014

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

No branches or pull requests

6 participants