Fixing typed HttpClient registration
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 orBaseAddress
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, theTClient
type must have a constructor that accepts anHttpClient
parameter. Additionally, theTClient
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.