Bypass HTTP browser cache when using HttpClient in Blazor WebAssembly

 
 
  • Gérald Barré

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:

C#
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:

C#
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.

C#
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:

Razor
@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.

C#
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:

Razor
@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:

Razor
@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

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub