HttpClient Best Practices

HttpClient Best Practices

Using HttpClient Wrong Can Break Your App

Let’s talk about something that might seem simple but can cause BIG problems if you’re not careful: HTTP clients. These are the tools your app uses to talk to other services on the internet. But if you use them the wrong way, your app can crash.


The Wrong Way: Creating a New HttpClient Every Time ❌

Imagine you’re building an app that needs to call an API. You think, “I’ll just create a new HttpClient every time I need to make a request. Easy, right?” So you write something like this:

public async Task<User> GetUserAsync(string username)
{
    using var client = new HttpClient(); // Danger! 🚨
    client.BaseAddress = new Uri("https://api.coolservice.com");
    client.DefaultRequestHeaders.Add("Authorization", "Bearer my-token");

    var user = await client.GetFromJsonAsync<User>($"users/{username}");
    return user;
}

Looks simple, right? But here’s the problem: HttpClient is not meant to be used this way!

Why This Is Bad

  1. Port Exhaustion: Every time you create a new HttpClient, it opens a new connection. If you do this too much, your app can run out of available ports. When this happens, your app crashes.

  2. Wasted Resources: Creating and disposing of HttpClient over and over is like buying a new phone every time you want to make a call. It’s wasteful and inefficient.

The Big Problem with Disposing Handlers

Here’s where things get tricky. When you dispose of a HttpClient, it also disposes of its HttpMessageHandler by default. This might seem fine, but remember: the handler manages the underlying connections. If you dispose of it, those connections are closed, and new ones have to be created the next time you make a request.

This is why creating and disposing of HttpClient instances over and over can lead to port exhaustion and DNS issues. It’s like constantly tearing down and rebuilding a bridge instead of reusing it.

HttpClientFactory is smart. When it creates a HttpClient, it sets disposeHandler to false. This means the handler isn’t disposed of when the HttpClient is disposed. Instead, the factory manages the handler’s lifecycle, reusing it when possible and disposing of it only when it’s no longer need.

explore source code : HttpClientLibrary

TL;DR: The Key Takeaways

  1. HttpClient’s constructors control whether the handler is disposed of.

  2. Disposing handlers closes connections, which can lead to port exhaustion.

  3. HttpClientFactory sets disposeHandler to false, reusing handlers and connections.

  4. Reusing handlers is the secret sauce to keeping your app fast and reliable.


The Right Way: Use HttpClientFactory ✅

Instead of creating HttpClient manually, you should use HttpClientFactory. Think of it as a smart manager that handles all the connections for you. It reuses connections, avoids port exhaustion, and makes your life easier.

public class CoolService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public CoolService(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    public async Task<User> GetUserAsync(string username)
    {
        var client = _httpClientFactory.CreateClient(); //No more "using"
        client.BaseAddress = new Uri("https://api.coolservice.com");
        client.DefaultRequestHeaders.Add("Authorization", "Bearer my-token");

        var user = await client.GetFromJsonAsync<User>($"users/{username}");
        return user;
    }
}

Why This Is the best way:

  1. Reuses Connections: HttpClientFactory manages a pool of connections, so you don’t waste resources.

  2. Short-Lived Clients: The HttpClient instances are meant to be used quickly and then discarded. The factory handles the cleanup for you.

  3. Handles DNS Changes: If the API’s IP address changes (common in cloud environments), HttpClientFactory adapts automatically. No more broken connections!


Level Up: Named and Typed Clients 🚀

Want to make your code even cleaner? Use named clients or typed clients. These let you pre-configure your HttpClient so you don’t have to repeat the same setup code every time.

Named Clients

services.AddHttpClient("CoolService", client =>
{
    client.BaseAddress = new Uri("https://api.coolservice.com");
    client.DefaultRequestHeaders.Add("Authorization", "Bearer my-token");
});

Then, use it in your service:

public async Task<User> GetUserAsync(string username)
{
    var client = _httpClientFactory.CreateClient("CoolService"); //pass in name of the client
    var user = await client.GetFromJsonAsync<User>($"users/{username}");
    return user;
}

Typed Clients

Even better, you can create a typed client. This is like having a custom-made HttpClient for your service.

First, define your typed client:

public class CoolService
{
    private readonly HttpClient _client;

    public CoolService(HttpClient client)
    {
        _client = client;
    }

    public async Task<User> GetUserAsync(string username)
    {
        return await _client.GetFromJsonAsync<User>($"users/{username}");
    }
}

Then, configure it in Startup.cs:

services.AddHttpClient<CoolService>(client =>
{
    client.BaseAddress = new Uri("https://api.coolservice.com");
    client.DefaultRequestHeaders.Add("Authorization", "Bearer my-token");
});

Now, you can inject CoolService directly, and the HttpClient will be ready to go. No more repetitive setup!


Watch Out: Typed Clients in Singleton Services ⚠️

Here’s a pro tip: Don’t use typed clients in singleton services. Why? Because typed clients are meant to be short-lived, and if they’re stuck in a singleton, they won’t react to DNS changes. It’s like using an old map forever, even though the city has changed. 🗺️

If you use a typed client in a singleton, configure the PooledConnectionLifetime to refresh connections periodically:

services.AddHttpClient<CoolService>(client =>
{
    client.BaseAddress = new Uri("https://api.coolservice.com");
    client.DefaultRequestHeaders.Add("Authorization", "Bearer my-token");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(15) // Refresh connections
})
.SetHandlerLifetime(Timeout.InfiniteTimeSpan);


TL;DR: Why HttpClientFactory Wins

  1. No Port Exhaustion: Reuses connections instead of creating new ones.

  2. Handles DNS Changes: Keeps your app up-to-date with changing IP addresses.

  3. Cleaner Code: Named and typed clients make your code easier to read and maintain.

  4. No Disposal Drama: Let the factory handle the lifecycle of HttpClient.

So, next time you’re tempted to create a new HttpClient, remember: HttpClientFactory is your friend. Use it, and keep your app running smoothly.