Fixing typed HttpClient registration

August 23rd 2024 .NET

Typed clients are one of the recommended usage patterns for HttpClient in .NET. Although it's easy to set up, you can just as easily set it up incorrectly. I had to troubleshoot such cases more than once and the culprit has almost always been the same.

The key part of setting up a typed client is registering it with the generic AddHttpClient method:

builder.Services.AddHttpClient<SampleService>(client =>
{
    client.BaseAddress = new Uri(
        builder.Configuration.GetValue<string>("BaseAddress")
            ?? throw new ConfigurationErrorsException(
                "BaseAddress configuration value is missing."
            )
    );
});

With this in place, a preconfigured HttpClient instance is going to be injected into your SampleService class:

public class SampleService(HttpClient httpClient)
{
    public async Task<int[]> GetData()
    {
        return await httpClient.GetFromJsonAsync<int[]>("data") ?? [];
    }
}

Since a typed client is only going to be injected into a single service (SampleService in my case), it's common to preconfigure it as part of the registration. I'm setting up its BaseAddress but I could also predefine one or more headers for example.

That's also how you usually notice that something is misconfigured in your project: the injected HttpClient instance is not configured as expected. In my case it wouldn't have the BaseAddress properly set which would result in the following exception:

System.InvalidOperationException: An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set.

In my experience, the most common reason for that is also having SampleService registered with dependency injection:

builder.Services.AddScoped<SampleService>();

If you do that, a default HttpClient will be injected into it instead of the default one. The documentation also mentions that you shouldn't be doing that (emphasis mine):

When registering a typed client with the AddHttpClient<TClient> method, the TClient type must have a constructor that accepts an HttpClient parameter. Additionally, the TClient type shouldn't be registered with the DI container separately.

So why would someone do that? Either because they didn't read the documentation (in full) or because they registered the service with DI when it didn't require an injected HttpClient instance and forgot about it when they added the HttpClient parameter and registered a typed client for it.

Why can it be difficult to troubleshoot? Because in a real-world project there's typically going to be many services registered with DI and also many typed clients. The typed client and the corresponding service are unlikely to be registered immediately one after the other. They might not even be registered in the same file.

You can find a sample project demonstrating this issue in my GitHub repository. The final commit contains a working configuration, but the one before it also registers the service separately which causes an incorrectly configured HttpClient to be injected and the test to fail.

Typed clients are a common pattern for using the HttpClient in .NET. If if doesn't work for you as expected, check whether the service into which you want the HttpClient to be injected is also separately registered with the DI. I find this to be a common reason for a default HttpClient to be injected instead of the correctly preconfigured one.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License