Introducing IHttpClientFactory in .NET Core - Overcoming Challenges with HttpClient - part 1
Introduction
In many projects, we have to interact with remote APIs to retrieve data or delegate actions to external services. Common scenarios include calling an Invoice Service API to publish invoices or utilizing an SMS Brand Name API to send messages. If you're an experienced .NET programmer, you've likely used HttpClient to send HTTP requests. However, HttpClient has known issues that can destabilize your application over time.
IHttpClientFactory, introduced in .Net Core 2.1, addresses two critical pitfalls associated with HttpClient:
- Socket Exhaustion: This occurs when your server runs out of available ports, preventing it from establishing TCP/IP connections to a remote API's server.
- DNS Change Issues: If a remote server's domain is reassigned to a new IP address, HttpClient may continue using the outdated IP, failing to recognize the change.
Analysis problems with HttpClient
Socket Exhaution
Before sending a request to a remote server, a TCP/IP connection is established. Each connection has a socket, which consists of a pair (port and IP) on each side. The client machine randomly selects a port to connect to the remote server's public port like the image below:
Because HttpClient implements the IDisposable interface, it is commonly wrapped in a using statement to ensure that unmanaged resources are released when the object goes out of scope, as shown below:
using (var insanceOfUnmanagedResourse = new DisposableClass())
{
// use instanceOfUnmanagedResource
}
This practice leads to the following code pattern:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.MapGet("/", async () =>
{
using HttpClient client = new HttpClient(); ?
client.BaseAddress = new Uri("https://randomuser.me/");
var response = await client.GetAsync("api");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
});
app.Run();
Each time a new request is sent to a remote API, the HttpClient is disposed at the end of the scope, requiring a new TCP/IP connection for subsequent requests. This process necessitates the creation of a new socket and the selection of a new random port, while the previously used port remains in a TIME_WAIT state for 240 seconds on Windows, unavailable for reuse. In high-load scenarios, this can lead to port exhaustion, preventing further external API calls.
DNS change problem
To mitigate the socket exhaustion issue, one might consider using a static HttpClient instance:
var builder = WebApplication.CreateBuilder(args);
// Create a static HttpClient instance
static readonly HttpClient SharedHttpClient = new HttpClient
{
BaseAddress = new Uri("https://randomuser.me/")
};
var app = builder.Build();
// Define a minimal API endpoint
app.MapGet("/", async () =>
{
var response = await SharedHttpClient.GetAsync("api");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
});
app.Run();
This approach prevents the disposal of HttpClient until the application terminates, solving the socket exhaustion issue. However, it introduces a potential DNS change problem. For instance, if the domain randomuser.me initially resolves to IP 104.21.90.128 but later changes to 113.1.2.3, the static HttpClient will continue to connect to the outdated IP address.
In this article, we've explored common issues with using HttpClient. In the next installment, I will discuss how IHttpClientFactory can resolve these challenges, along with additional benefits it offers.