In this article, we are going to learn about handling HTTP errors in the Blazor WebAssembly application. We are going to start with a simple example, and then improve the solution until we implement global HTTP error handling with the Blazor WebAssembly interceptor. With this, we are going to create a centralized place to handle our HTTP errors sent from the Web API server application.
Let’s get started.
Projects Introduction
As we said, in our source-code repository, you are going to find two folders with two projects inside each of them. We took these projects from our Blazor WebAssembly series. If you want to gain additional knowledge on Blazor WebAssembly project development, feel free to visit the linked page, which contains all the articles from the series and more.
One project is the ASP.NET Core Web API. We have enabled automatic migration for that project, so all you have to do is to modify the connection string in the appsettings.json
file and run the application. It will create a new database for you with the required data. For this article, we will focus only on the ProductsController
class under the Controllers
folder:
[Route("api/products")] [ApiController] public class ProductsController : ControllerBase { private readonly IProductRepository _repo; public ProductsController(IProductRepository repo) { _repo = repo; } [HttpGet] public async Task<IActionResult> Get() { var products = await _repo.GetProducts(); return Ok(products); } }
This is a simple logic where we fetch all the products from the database. We are going to modify the return
statement in order to send different error responses to the client application.
The second project is the Blazor WebAssembly project:
As you can see, we have the HttpRepository
folder and the Products.razor
page inside the Pages
folder. We are mentioning the Products
page and the HttpRepository
files because these files will be the main focus of this article.
If we open the ProductHttpRepository.cs
file, we are going to see the code that sends an HTTP request to the API and parses the response into the list of products:
public class ProductHttpRepository : IProductHttpRepository { private readonly HttpClient _client; public ProductHttpRepository(HttpClient client) { _client = client; } public async Task<List<Product>> GetProducts() { var response = await _client.GetAsync("products"); var content = await response.Content.ReadAsStringAsync(); var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return products; } }
Now, if we start both applications and navigate to the Products menu item, we are going to see all the products listed on the page:
If you see this, then everything is prepared for further logic.
Handling HTTP Errors Inside the Repository File
Now, let’s slightly modify the ProductsController
to return an error response:
[HttpGet] public async Task<IActionResult> Get() { var products = await _repo.GetProducts(); return StatusCode(500, "Something went wrong."); }
We can start our applications again, and navigate to the Products
menu item:
As we can see, we have the yellow box with the message, which is the Blazor’s default way of handling errors. We see two error messages in the console window. The first one suggesting that an error has happened with a certain status code, and the second one suggesting that our deserialization failed with an unhandled exception as a result. We should avoid unhandled exceptions at all costs.
So, let’s see what we can do about it.
The first thing we can do is using the HttpResponseMessage mechanism to verify that the response contains a successful status code:
public async Task<List<Product>> GetProducts() { var response = await _client.GetAsync("products"); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return products; }
With the EnsureSuccessStatusCode
method, we prevent any further code execution if our response is not successful:
From these messages, we can see that we have prevented the JsonSerializer error because as soon as we notice that our response is not successful, we stop the code execution and throw an exception. This is an okay solution if you don’t want to do any specific actions with the error response or don’t want to use the message sent from the server inside the response.
Using the IsSuccessStatusCode Property to Check HTTP Responses
Now, let’s modify our this solution a bit:
public async Task<List<Product>> GetProducts() { var response = await _client.GetAsync("products"); if (!response.IsSuccessStatusCode) throw new ApplicationException($"The response from the server was not successfull, reason: {response.ReasonPhrase}"); var content = await response.Content.ReadAsStringAsync(); var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return products; }
Here we use the IsSuccessStatusCode property to check whether the response is successful or not. If it’s not, we throw an ApplicationException with our custom message and provide the reason as well:
This time, we can see our custom message and we can see the reason why the HTTP response is not successful. But we still can’t see the message sent from the server. If we want to see it, we can pass the content
variable as a parameter to the exception:
public async Task<List<Product>> GetProducts() { var response = await _client.GetAsync("products"); var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) throw new ApplicationException($"Reason: {response.ReasonPhrase}, Message: {content}"); var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return products; }
If we check our result now:
We can see the reason and the message sent from the server.
Providing a Better User Experience with HTTP Error Handling
With all the previous techniques, we can handle the HTTP errors but they are not user-friendly at all. Forcing users to inspect console logs is not good practice. What we want is to enable a good UI for the users to make them aware of what is going on and why the action failed.
If you inspect the Pages
folder, you are going to find the CustomNotFound
page. This is our page that handles errors with the 404 status code. That said, let’s create two new Razor components in the same folder.
The CustomUnauthorized.razor
page:
@page "/unauthorized" <h3>You are not authorized.</h3>
And the CustomInternalServerError.razor
page:
@page "/500" <h3>Something went really wrong. Please contact administrator.</h3>
As you can see, these are pretty simple pages, but you can provide parameters to them and make them even more user-friendly. If you want to learn more about the Blazor components and how to pass parameters to them, you can visit our Blazor WebAssembly series of articles.
Now, we can modify the ProductHttpRepository.cs
file:
public class ProductHttpRepository : IProductHttpRepository { private readonly HttpClient _client; private NavigationManager _navManager; public ProductHttpRepository(HttpClient client, NavigationManager navManager) { _client = client; _navManager = navManager; } public async Task<List<Product>> GetProducts() { var response = await _client.GetAsync("products"); var content = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { var statusCode = response.StatusCode; switch (statusCode) { case HttpStatusCode.NotFound: _navManager.NavigateTo("/404"); break; case HttpStatusCode.Unauthorized: _navManager.NavigateTo("/unauthorized"); break; default: _navManager.NavigateTo("/500"); break; } throw new ApplicationException($"Reasong: {response.ReasonPhrase}"); //or add a custom logic here } var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return products; } }
Here, we inject the NavigationManager
service and use it to navigate the user to the appropriate page. We use the StatusCode
property to extract the status code from the response and based on it redirect the user. Also, we throw an application exception to stop the execution flow. If you don’t want to throw this exception, you can always add your custom logic. For example, in this case, we could just return an empty product list return new List<Product>();
.
Finally, if you don’t want that yellow box to appear every time you throw an exception, you can open the wwwroot/index.html
file and comment out the code responsible for it:
<div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
Now, we can test this solution:
As you can see, we are redirected to the right page, without the yellow box to interrupt the user, and we can see the message if we open the console window.
We can test this more by modifying the Get
action to return NotFound
response:
[HttpGet] public async Task<IActionResult> Get() { var products = await _repo.GetProducts(); return NotFound(); }
And if we start both applications and navigate to Products
:
We can see a different page with a different message.
But the fun part is yet to come.
Global HTTP Error Handling using Interceptor
We can use the interceptor in the Blazor WebAssembly application to centralize the error handling logic. Without it, we would have to repeat the previous HTTP error handling logic in all the methods we are using HttpClient from. Of course, we don’t want to do that.
So, let’s see how to improve our solution.
First, let’s install the required library:
PM> Install-Package Toolbelt.Blazor.HttpClientInterceptor -Version 9.2.0-preview.1
After the installation, we want to modify the Program.cs
class, to register this interceptor and to attach it for the specific HttpClient:
public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddHttpClient("ProductsAPI", (sp, cl) => { cl.BaseAddress = new Uri("https://localhost:5011/api/"); cl.EnableIntercept(sp); }); builder.Services.AddScoped( sp => sp.GetService<IHttpClientFactory>().CreateClient("ProductsAPI")); builder.Services.AddHttpClientInterceptor(); builder.Services.AddScoped<IProductHttpRepository, ProductHttpRepository>(); await builder.Build().RunAsync(); }
Here, we use the AddHttpClient
method overload that uses the action delegate as a second parameter that accepts both the ServiceProvider
(sp) and HttpClient
(cl) as parameters. Inside that action, we use the EnableIntercept
method to enable the interceptor for this specific HttpClient
, and we provide the ServiceProvider
as an argument.
Also, with the AddHttpClientInterceptor
method, we register the interceptor in the service collection. For this to work properly, we have to use the Toolbelt.Blazor.Extensions.DependencyInjection
namespace.
Creating the custom HttpInterceptorService
Now, let’s create a new HttpInterceptorService.cs
class:
public class HttpInterceptorService { private readonly HttpClientInterceptor _interceptor; private readonly NavigationManager _navManager; public HttpInterceptorService(HttpClientInterceptor interceptor, NavigationManager navManager) { _interceptor = interceptor; _navManager = navManager; } public void RegisterEvent() => _interceptor.AfterSend += InterceptResponse; privatevoid InterceptResponse(object sender, HttpClientInterceptorEventArgs e) { string message = string.Empty; if (!e.Response.IsSuccessStatusCode) { var statusCode = e.Response.StatusCode; switch (statusCode) { case HttpStatusCode.NotFound: _navManager.NavigateTo("/404"); message = "The requested resorce was not found."; break; case HttpStatusCode.Unauthorized: _navManager.NavigateTo("/unauthorized"); message = "User is not authorized"; break; default: _navManager.NavigateTo("/500"); message = "Something went wrong, please contact Administrator"; break; } throw new HttpResponseException(message); } } public void DisposeEvent() => _interceptor.AfterSend -= InterceptResponse; }
First, we inject two services – NavigationManager
and HttpClientInterceptor
. Then, we can see three methods. The first method adds the event to the AfterSend
event handler. The last method removes it from the same event handler. The AfterSend
event handler will trigger the event as soon as our application receives the HTTP response from the API. This interceptor also has the BeforeSend
event handler which we can use before we send a request. To see this in action you can read our Refresh Token with Blazor WebAssembly article.
The second method – InterceptResponse
– is the main logic (event) of our interceptor service. In this method, we use the HttpClientInterceptorEventArgs e
parameter to access the intercepted response. Once we access the response, the logic is almost the same as we had in the ProductHttpRepository
class. Just in this case, after each navigation, we populate the message and use it with the custom exception.
Of course, the HttpResponseException is our custom one, and we need to create it:
[Serializable] internal class HttpResponseException : Exception { public HttpResponseException() { } public HttpResponseException(string message) : base(message) { } public HttpResponseException(string message, Exception innerException) : base(message, innerException) { } protected HttpResponseException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
Nice.
Let us mention again, that this is just one example, you can always use your custom logic here instead of throwing a custom exception. But if your custom logic doesn’t stop the flow and it reaches the GetProducts method, you have to handle it there as well due to the possible deserialization problem.
Now, let’s register this service in the Program.cs
class:
builder.Services.AddScoped<HttpInterceptorService>();
And just like that, our service is prepared.
Using Interceptor
Before we start using our global HTTP error handler in our application, we want to remove the code from the GetProducts
method inside the ProductHttpRepository
class:
public async Task<List<Product>> GetProducts() { var response = await _client.GetAsync("products"); var content = await response.Content.ReadAsStringAsync(); var products = JsonSerializer.Deserialize<List<Product>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return products; }
This is the original state of this method.
Now, let’s modify the Products.razor.cs
class. This is the partial component’s class where we use the ProductHttpRepository
:
public partial class Products : IDisposable { public List<Product> ProductList { get; set; } = new List<Product>(); [Inject] public IProductHttpRepository ProductRepo { get; set; } [Inject] public HttpInterceptorService Interceptor { get; set; } protected async override Task OnInitializedAsync() { Interceptor.RegisterEvent(); ProductList = await ProductRepo.GetProducts(); } public void Dispose() => Interceptor.DisposeEvent(); }
As you can see this class implements the IDisposable
interface. We also inject the custom HttpInterceptorService
, register the event in the OnInitializedAsync
lifecycle method, and dispose of the event in the Dispose
method.
If we start both applications and navigate to the Products menu item, we are going to see that the application redirects us to the CustomNotFound
page. So, it works as before, but this time with a centralized solution and better implementation because we don’t have to duplicate our code:
Excellent.
If you want, you can return the Unauthorized response from the API, and you will see the appropriate page for that HTTP response.
Conclusion
We did a great job here.
Now we know how to implement a global HTTP error handling mechanism using the HTTP interceptor in the Blazor WebAssembly application. This implementation centralizes the place for handling HTTP errors and prevents code duplication in each method.
Of course, you can always add your custom logic to this interceptor service, but the base logic is here, everything else should be an easy job.
Until the next article…
All the best.
I tried to implement the interceptor but could not find EnableIntercept.
(I DID install your nugt package but it was version 10.1.0)
<PackageReference Include=”Toolbelt.Blazor.HttpClientInterceptor” Version=”10.1.0″ />
builder.Services.AddHttpClient(“ProductsAPI”, (sp, cl) =>
{
cl.BaseAddress = new Uri(“https://localhost:5011/api/”);
cl.EnableIntercept(sp);
});
So I’m stuck. 🙁
Hi Randy. Well, to be honest, I just tried a new .NET 6 solution and everything works well. So either you can use a default HttpClient implementation that the Blazor WASM app offers:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }.EnableIntercept(sp));
Or you can choose the one we have in this article, just for that you need this package first: Microsoft.Extensions.Http, to be able to use the AddHttpClient method.
We said in the article that we borrowed both projects from our Blazor WebAssembly series and the AddHttpClient method was already implemented.
Nice post! What about logging (actually “handling”) unhandled exceptions on Blazor WASM to server API? Any thoughts, “best practices”?
Hi Dmitry, curious if you ever got this figured out? I am implementing error handling using this page as a template and cannot seem to catch the exception in the HttpInterceptorService. The only way I can see to handle is to implement try/catch blocks in the API calls within components…which seems to defeat a lot of the purpose for using the intercept at all for this purpose.
You could add custom handlers in Blazor Wasm project for calls to your server APIs like this https://stackoverflow.com/questions/73259220/how-can-i-add-http-interceptors-in-blazor-wasm
Has anyone managed to get the HttpClientInterceptor to handle showing any message content from the API? I have tried many ways and still i cant show any errors that i want to capture. I’ve tried these and still nothing.
example 1:
var capturedContent = await e.GetCapturedContentAsync();
message = await capturedContent.ReadAsStringAsync();
example 2:
message = JsonConvert.DeserializeObject<dynamic>(e.Response.Content.ReadAsStringAsync().Result);
example 3:
message = e.Response.Content.ReadAsStringAsync().Result;
example 4:
message = await e.Response.Content.ReadAsStringAsync();
Can anyone explain why these dont work?
Is it possible to display a dialog box from the InterceptResponse method and show it on the same page that is making the API call?
I am trying to catch an API error and display a MudBlazor dialog box from the InterceptResponse method. They don’t want to navigate to another page when there are timeout errors or a bad request that can be resent. It seems like it should work but the dialog doesn’t display and it doesn’t throw an error. The dialog box displays fine when called from that page but they have made a requirement to call it from the InterceptResponse method so that the dialog doesn’t have to be implemented on all the client side pages that make those types of api calls.
This is the code I have using MudBlazor Dialog.
//Stay in same page
case HttpStatusCode.BadRequest:
Dialog.Show<ErrorDialog>(CustomError.title, parameters, dialogOptions);
message = “Invalid request. Please try again.”;
break;
Well, it should be possible. In our Blazor WebAssembly video course, we used Blazored Toast to show error notifications on each page, which is implemented in the interceptor service.
I bought the course which module is that in?
Hello John. Thank you for the purchase. In module 7, you will find the error handling logic, and in module 8, you will find how we use the Blazored Toast to show notifications.
Thanks for the great tutorial!
I have one questions though.
At the moment, we need to repeat the interceptor setup in every service/component we create. What would be your recommendation to somehow automatically get this global error handling when creating a new component? So that I don’t need to do the setup for each new component explicitly.
To be honest I didn’t try to improve that just because the service injection, using IDispsable interface, and calling the register event method is not some amount of code. At least not for my standards 🙂 Basically, you will not use this for all the components in your project, so I still think it is a good setup as-is. Of course, if you find a way to improve this, please share it with us, I will be more than happy to learn it from you 🙂
Hi a Question. Should you not call DisposeEvent() after each api call?
Hello. By the information from the interceptor GitHub page, you should do it when you dispose of your component.
This is a quoted text:
“Note: Please remember that if you use HttpClientInterceptor to subscribe BeforeSend/BeforeSendAsync/AfterSend/AfterSendAsync events in Blazor components (.razor), you should unsubscribe events when the components is discarded.”
Hi can not get it to work in my own solution. The HttpClientInterceptorEventArgs in “private void InterceptResponse(object sender, HttpClientInterceptorEventArgs e)” is always null.
Could it be because I am using
b.Services.AddMsalAuthentication(options =>
{
options.UserOptions.RoleClaim = "role";
b.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
}).AddAccountClaimsPrincipalFactory();
Everything
public static async Task Main(string[] args)("#app");
{
var b = WebAssemblyHostBuilder.CreateDefault(args);
b.RootComponents.Add
// Setup the http client();().CreateClient("Api"));
var samsonApiUrl = new Uri(b.HostEnvironment.BaseAddress + "api/");
b.Services.AddHttpClient("Api", (serviceProvider,client) =>
{
client.BaseAddress = samsonApiUrl;
client.EnableIntercept(serviceProvider);
}).AddHttpMessageHandler
// Supply HttpClient instances that include access tokens when making requests to the server project
b.Services.AddScoped(sp => sp.GetRequiredService
b.Services.AddHttpClientInterceptor();
var scope = b.Configuration.GetSection("Scope").Value;();
b.Services.AddMsalAuthentication(options =>
{
options.UserOptions.RoleClaim = "role";
b.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add(scope);
}).AddAccountClaimsPrincipalFactory
b.Services.AddOptions();
b.Services.AddAuthorizationCore();
b.Services.AddScoped();();
b.Services.AddScoped
await b.Build().RunAsync();
}
I really don’t think that should be the reason for your issue. I am not sure why you have that issue though. This service should only intercept the Http request and get the data from it.
Just grasping for a straw (:
I have asked for help on stackoverflow
Hi where are the HttpResponseException handled? I don’t see any try/catch block.
Okay to answer my own question (-:
https://github.com/jsakamoto/Toolbelt.Blazor.HttpClientInterceptor/blob/master/Toolbelt.Blazor.HttpClientInterceptor/HttpClientInterceptorHandler.cs
Am I correct in saying this is only for Blazor WASM? This can’t be used with Blazor Server? If not do you know a similar way to do this in Blazor Server?
Yes you are right for both, it is only ofr Blazor WASM because it is a client application. To use global error handler inside the blazor server you don’t need an HTTP interceptor, because you don’t have Http calls, at least not for your internal application. I didn’t try it, but I believe you can use ExceptionHandler like we did in this article: https://code-maze.com/global-error-handling-aspnetcore/
Thanks for the fast response, will definitely take a look. Have you perhaps looked at this ASP.NET Core Blazor Global exception handling ? It uses an Error component (the expection handling would be for the frontend)
Excellent explanation. I successfully implemented the Interceptor. Super
Thank you, I am glad this article helped you.
I request you, please change this download code, by taking the latest VS2019 templates of .net Core Blazor. Otherwise, those who are coming new to .net Blazor, will have difficult in understanding the code. Especially the Program and Startup.cs. Otherwise you explanation is a breeze.
Well to be honest, the people who are new with Blazor, shouldn’t be spending time on learning how to globally handle errors but should learn about Blazor WASM first 🙂 Once they do that, our code sample would be quite easy to understand. Also the Blazor WASM Hosted app template is almost similar to our example, just we have two separated projects.
I following step to step but it not working in my project and not console problem in visual studio.
Try downloading our source code. And then compare it to yours. It has to work, this example was tested multiple times.
This is very nice, and I’ve got it working, but I’d like to show a notification with Radzen instead of redirecting to an error page.
The trouble is in the event of the api server being down and e.Reponse being null, if I just show the notification in the InterceptResponse() method and then return, the underlying exception halts the application and the page freezes.
Is there any way to supress this so the user just gets the notification and the application remains responsive?
Secondly, I’ve noticed that when I do get an error response my notification gets shown twice, as the interceptor code is run twice – is this something to do with a pre-flight request being made to the server? How can I stop this happening?
Thanks,
Tony
If you response is null, you should create a separate guard for it, checking whether this is a case. If it is, you can either notify your user somehow or navigate them to 500 page, but you have to stop the further execution flow by throwing HttpResponseException or some custom one. This shouldn’t stop your app, and it should be responsive.
Regarding the second problem, maybe you are combining async requests with sync interceptor events. Try using the async ones AfterSendAsynd nad BeforeSendAsync.
Thanks for the swift reply, Marinko
Hi,
Great solution which works superbly in my trial app. However, I would like to display the user specific messages returned from the API. Is there any way to grab an error object from the ‘e’ object in the InterceptResponse event in the HttpInterceptorService class? If not, can you suggest a way?.
Hello Chas. You can use the e.Response.Content for that purpose. For example on the server side: return NotFound(“Some message here”);
There is a custom message in the response.
Now on the client side you should add:
var content = await e.Response.Content.ReadAsStringAsync();
With this you will extract the custom message from a response.
Pay attention that this is an async operation, so, you have to modify entire class to support async await operations:
public void RegisterEvent() => _interceptor.AfterSendAsync += InterceptResponse;
private async Task InterceptResponse…
public void DisposeEvent() => _interceptor.AfterSendAsync -= InterceptResponse;
I hope this helps.
Best regards.
Hi Marinko,(await e.Response.Content.ReadAsStringAsync());
Thank you so much for the solution to this which I implemented and tested successfully,
To standardise the error returns from the API I use an error object, so I adapted your solution as follows:
var apiResopnse = JsonConvert.DeserializeObject
and in the switch block:
case HttpStatusCode.NotFound:
_navManager.NavigateTo(“/404?message=” + apiResponse.Message);
This all works exactly as expected.
Thanks again for your help.
Best regards
The package used Toolbelt.Blazor.HttpClientInterceptor is no doubt an excellent option. I don’t see anything comparable in the new features for Blazor .Net 5 that would be comparable. I have read the license available on github. Obviously, I am no lawyer. How can I be sure that adding it to a project that will be distributed commercially to hundreds of customers will have no ill legal ramifications down the road? What if any additional information would I need to add to our commercial product when this package via nuget? Examples of this would be… original license of the package, acknowledgment of original author etc.
Hello Jamy. I completely understand you and approve your way of thinking. I was talking with author of the library, when I was writing the Blazor Refresh Token article, and I suggested about awaitable interceptions, which are now part of the package. But honestly, I never asked him about the licence. I think it would be nice knowing these information (ones you mentioned in your comment). As you said, I am not a lawyer as well.