Bypass HTTP browser cache when using HttpClient in Blazor WebAssembly
Blazor WebAssembly relies on the browser to execute web requests. Every call you make using HttpClient
are executed using the fetch
API (documentation) provided by the browser.
By default, the browser uses the Cache-Control
header to know if a response should be cached and how long it should be cached. When there is no header in the response, the browser has its heuristic. Sometimes, people add a parameter in the query string with a random value to ensure the URL cannot be served from the cache. A better way to do it is to use the fetch cache-control API to control the cache behavior.
Blazor WebAssembly allows setting the value of the cache-control when executing a request. This means you can bypass the cache if needed by setting the right value in the request options. Available options are exposed by the BrowserRequestCache
enumeration. As the values are the same as the one exposed by the fetch
API, the documentation is the same as the one on MDN. Here's a summary of the available options:
Default
: The browser looks for a matching request in its HTTP cache.ForceCache
: The browser looks for a matching request in its HTTP cache. If there is a match, fresh or stale, it will be returned from the cache. If there is no match, the browser will make a normal request, and will update the cache with the downloaded resource.NoCache
: The browser looks for a matching request in its HTTP cache. If there is a match, fresh or stale, the browser will make a conditional request to the remote server. If the server indicates that the resource has not changed, it will be returned from the cache. Otherwise, the resource will be downloaded from the server and the cache will be updated. If there is no match, the browser will make a normal request, and will update the cache with the downloaded resource.NoStore
: The browser fetches the resource from the remote server without first looking in the cache, and will not update the cache with the downloaded resource.OnlyIfCached
: The browser looks for a matching request in its HTTP cache. Mode can only be used if the request's mode is "same-origin". If there is a match, fresh or stale, it will be returned from the cache. If there is no match, the browser will respond with a 504 Gateway timeout status.Reload
: The browser fetches the resource from the remote server without first looking in the cache, but then will update the cache with the downloaded resource.
In Blazor WebAssembly you can use SetBrowserRequestCache
on a HttpRequestMessage
to set the Request Cache mode:
using var httpClient = new HttpClient();
// First request => Download from the server and set the cache
using var request1 = new HttpRequestMessage(HttpMethod.Get, "https://www.meziantou.net");
using var response1 = await httpClient.SendAsync(request1);
// Second request, should use the cache
using var request2 = new HttpRequestMessage(HttpMethod.Get, "https://www.meziantou.net");
using var response2 = await httpClient.SendAsync(request2);
// Third request, use no-cache => It should revalidate the cache
using var request3 = new HttpRequestMessage(HttpMethod.Get, "https://www.meziantou.net");
request3.SetBrowserRequestCache(BrowserRequestCache.NoCache);
using var response3 = await httpClient.SendAsync(request3);
You can check using the debug tools if the request is served from the cache. In this case:
- The first request fetches the data from the server and update the cache
- The second request is served from the cache as the cache is fresh ⇒ "(disk cache)" in the screenshot
- The third request is revalidated as it uses "no-cache" ⇒ Status code 304 as the cache is valid
Note that other fetch options are exposed with the following methods: SetBrowserRequestCache
, SetBrowserRequestMode
, SetBrowserRequestIntegrity
, SetBrowserResponseStreamingEnabled
, SetBrowserRequestCredentials
.
#Using a HttpMessageHandler to set the configuration for all messages
The SetBrowserRequestXXX
are per message. This means you need to create a message manually and set the option each time. This means you cannot set the options when using shorthand methods such as HttpClient.GetAsync
or HttpClient.GetFromJsonAsync
.
You can use an HttpMessageHandler
or one of its sub-classes to modify the options of all messages. Here's an example:
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Http;
public sealed class DefaultBrowserOptionsMessageHandler : DelegatingHandler
{
public DefaultBrowserOptionsMessageHandler()
{
}
public DefaultBrowserOptionsMessageHandler(HttpMessageHandler innerHandler)
{
InnerHandler = innerHandler;
}
public BrowserRequestCache DefaultBrowserRequestCache { get; set; }
public BrowserRequestCredentials DefaultBrowserRequestCredentials { get; set; }
public BrowserRequestMode DefaultBrowserRequestMode { get; set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Get the existing options to not override them if set explicitly
IDictionary<string, object> existingProperties = null;
if (request.Properties.TryGetValue("WebAssemblyFetchOptions", out object fetchOptions))
{
existingProperties = (IDictionary<string, object>)fetchOptions;
}
if (existingProperties?.ContainsKey("cache") != true)
{
request.SetBrowserRequestCache(DefaultBrowserRequestCache);
}
if (existingProperties?.ContainsKey("credentials") != true)
{
request.SetBrowserRequestCredentials(DefaultBrowserRequestCredentials);
}
if (existingProperties?.ContainsKey("mode") != true)
{
request.SetBrowserRequestMode(DefaultBrowserRequestMode);
}
return base.SendAsync(request, cancellationToken);
}
}
In the program.cs
file, you can update the declaration of the HttpClient
in the services builder.
builder.Services.AddTransient(sp => new HttpClient(new DefaultBrowserOptionsMessageHandler(new WebAssemblyHttpHandler()) // or new HttpClientHandler() in .NET 5.0
{
DefaultBrowserRequestCache = BrowserRequestCache.NoStore,
DefaultBrowserRequestCredentials = BrowserRequestCredentials.Include,
DefaultBrowserRequestMode = BrowserRequestMode.Cors,
})
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress),
});
Finally, you can use the HttpClient
from any page/component using dependency injection:
@page "/fetchdata"
@inject HttpClient Http
@code {
protected override async Task OnInitializedAsync()
{
await Http.GetFromJsonAsync<MyModel[]>("api/weather.json");
}
}
Now, every request made using this HttpClient
will use the default browser options! There is no need to set them on each request.
#Using dependency injection and IHttpClientFactory
You can inject an HttpClient instance into a razor component using the interface IHttpClientFactory
. Using DI allows having a single place to configure all HttpClients used by an application and adding cross-cutting concepts such as a retry policy or logs. In a Blazor WebAssembly application, you may need to add the NuGet package Microsoft.Extensions.Http
to your application.
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// Register the Message Handler
builder.Services.AddScoped(_ => new DefaultBrowserOptionsMessageHandler
{
DefaultBrowserRequestCache = BrowserRequestCache.NoStore
});
// Register a named HttpClient with the handler
// Can be used in a razor component using:
// @inject IHttpClientFactory HttpClientFactory
// var httpClient = HttpClientFactory.CreateClient("Default");
builder.Services.AddHttpClient("Default", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<DefaultBrowserOptionsMessageHandler>();
// Optional: Register the HttpClient service using the named client "Default"
// This will use this client when using @inject HttpClient
builder.Services.AddScoped<HttpClient>(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("Default"));
await builder.Build().RunAsync();
}
}
Then, you can use the HttpClient
from any page/component using dependency injection:
@page "/fetchdata"
@inject HttpClient Http
@code {
protected override async Task OnInitializedAsync()
{
await Http.GetFromJsonAsync<MyModel[]>("api/weather.json");
}
}
Or, you can use it with an IHttpClientFactory
:
@page "/fetchdata"
@inject IHttpClientFactory HttpClientFactory
@code {
protected override async Task OnInitializedAsync()
{
var client = HttpClientFactory.CreateClient("Default");
forecasts = await client.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
}
}
#Additional resources
- Source code of the post
- Make HTTP requests using IHttpClientFactory in ASP.NET Core
- Use IHttpClientFactory to implement resilient HTTP requests
Do you have a question or a suggestion about this post? Contact me!