In this post I would like to discuss some of the good and bad ways to use the HttpClient
. I am going to start from the basics.
What is the problem with the following code?
public async Task<string?> GetContentAsync(string url)
{
var client = new HttpClient();
var result = await client.GetStringAsync(url);
return result;
}
Problem is that the HttpClient
is not being properly disposed, as it implements IDisposable interface. So, let’s do it.
public async Task<string?> GetContentAsync(string url)
{
var client = new HttpClient();
var result = await client.GetStringAsync(url);
client.Dispose();
return result;
}
Right? Well, not quite, because if GetStringAsync
throws an exception, we will not reach the dispose call. Let’s fix it.
public async Task<string?> GetContentAsync(string url)
{
var client = new HttpClient();
string? result;
try
{
result = await client.GetStringAsync(url);
}
finally
{
client.Dispose();
}
return result;
}
Since finally block will always execute, the client will get disposed. And shorthand code below actually achieves the same thing.
public async Task<string?> GetContentAsync(string url)
{
using var client = new HttpClient();
var result = await client.GetStringAsync(url);
return result;
}
The using statement will take care of calling dispose when client is no longer in scope. So, are we good?
This is really beginning of what I wanted to discuss. The last code block is probably fine for Computer Science 101. However, in a production application where this method might be called thousands of times we will eventually get a nasty System.Net.Sockets.SocketException
. What’s worse is that it will probably pass all unit tests and QA process successfully.
The problem is that when HttpClient
is disposed, the underlying sockets are not immediately released, leading to a situation where you may be creating HttpClients
faster than releasing all the resources, hence the exception. First thought might be to use a Singleton pattern on the HttpClient
so it is only initialized once and used throughout the application. However, there is an issue with that as well because your HttpClient
will not respond to DNS changes unless constructed correctly. So the recommended solution is dependency injection of HttpClientFactory
. Microsoft has good documentation on different ways of using the factory. My favorite pattern is the Typed client.
public class HttpService
{
private readonly HttpClient _client;
public HttpService(HttpClient client)
{
_client = client;
}
public async Task<string?> GetContentAsync(string url)
=> await _client.GetStringAsync(url);
}
And register the service as follows.
builder.Services.AddHttpClient<HttpService>();
We can also utilize Microsoft.Extensions.Http.Polly
to add some resiliency and exponential backoff on retries (as well as configure things like global message handlers, request headers and base URL on the client).
var httpRetryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt
=> TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
builder.Services.AddHttpClient<HttpService>()
.AddPolicyHandler(httpRetryPolicy);
Voila! The above solution will avoid socket exhaustion and respond to DNS changes!
However, sometimes the only option is to build an HTTP service without any dependencies that will be used as a Singleton. I’ve had this happen to me when I was asked to write a class library that integrates with a third party API, which would be consumed by legacy .NET Framework application written in VB.NET. Luckily, there is an alternative solution to achieve the same HttpClient
stability as above without using dependency injection and HttpClientFactory
.
NOTE: The class is sealed
because we did not implement Dispose pattern to allow for safe inheritance.
The above alternative will also avoid socket exhaustion and respond to DNS changes, when used as a long-lived Singleton!