How to use HttpClient correctly to avoid socket exceptions

Published: Monday 20 May 2024

The HttpClient class is used to send HTTP requests in an ASP.NET Core application.

It's commonly used to send requests to external Web API's using the GET method.

We'll look at how to implement it in an ASP.NET Core Web API and show you the correct way to use it.

The different ways to create a HttpClient instance

There are a number of ways to create a HttpClient instance in an ASP.NET Core Web API.

Creating a HttpClient and dispose once used

We could create a new instance of it every time we need to make a HTTP request. As it inherits the IDisposable interface, we can use the using statement which will dispose of it once it's done.

// IHttpService.cs
public interface IHttpService
{
	Task<string> ReadAsync();
}
// HttpService.cs
public class HttpService : IHttpService
{
	public async Task<string> ReadAsync()
	{
		using var httpClient = new HttpClient();

		httpClient.BaseAddress = new Uri("https://dummyjson.com");

		var response = await httpClient.GetAsync($"/products");

		return await response.Content.ReadAsStringAsync();
	}
}

However, the HttpClient class is only meant to be initalised once per application. This is because each HttpClient instance uses its own connecton pool which involves using an available TCP port on the server.

When the HttpClient instance is disposed, it will dispose the connection pool, but doesn't release the TCP port immediately.

As a result, too many of these requests in a short period of time will mean that there will not be enough available ports to make a connection and you'll start to have socket exceptions when making API requests.

In-addition, if you wish to create another instance of HttpClient, you also have to re-establish the connection, causing unnecessary overhead.

This is not a recommended way of doing it.

Creating the HttpClient as a singleton instance

A much better way is to create the HttpClient instance as a singleton.

In the Program.cs file, you can create the instance inside the AddSingleton extension method in the IServiceCollection instance.

// Program.cs
var builder = WebApplication.CreateBuilder(args);

...

// Creates a new HttpClient instance as a singleton
builder.Services.AddSingleton((serviceProvider) => new HttpClient());

var app = builder.Build();

...

app.Run();

To use it as part of dependency injection, you just inject the HttpClient instance into the class.

// HttpService.cs
public class HttpService : IHttpService
{
	private readonly HttpClient _httpClient;

	public HttpService(HttpClient httpClient)
	{
		_httpClient = httpClient;
	}

	public async Task<string> ReadAsync()
	{
		var response = await _httpClient.GetAsync($"https://dummyjson.com/products");

		if (!(response?.IsSuccessStatusCode ?? false))
		{
			return string.Empty;
		}

		return await response.Content.ReadAsStringAsync();
	}
}

This means that there is only one connection pool open for the lifetime of the application.

However, there are some drawbacks with this method.

Setting the BaseAddress

If you get this error:

This instance has already started one or more requests. Properties can only be modified before sending the first request.

This means you have set the BaseAddress property of the HttpClient. Once it's been set, you can't set it again.

DNS issues

Once the application has started, it won't update the DNS for the lifetime of the application. This can cause an issue if the host that you are connecting to has frequent DNS updates.

However, this can be significantly reduced by creating a new instance of SocketsHttpHandler and setting the PooledConnectionLifetime. This is then passed in as a parameter to the HttpClient.

// Program.cs
var builder = WebApplication.CreateBuilder(args);

...

// Creates a new HttpClient instance as a singleton.
// Sets the connection lifetime to 5 minutes.
builder.Services.AddSingleton((serviceProvider) => new HttpClient(new SocketsHttpHandler
{
	PooledConnectionLifetime = TimeSpan.FromMinutes(5)
}));

var app = builder.Build();

...

app.Run();

With this example, it will update the DNS every five minutes.

This is much better, but there is another way.

Creating a HttpClient with IHttpClientFactory

Using the IHttpClientFactory instance to create HttpClient instances.

IHttpClientFactory is used as part of dependency injection and is good at HttpClient instance management, particularly:

  • Managing the number of HttpClient instances which are reusable across multiple requests.
  • Managing the connection pools, meaning it reuses connections across multiple requests
  • Managing the lifetime of a HttpClient instance

These benefits ensures that you don't have any connection pool issues as a result of too many TCP ports being open.

It also avoids common DNS problems that can occur.

One other major benefit with using the IHttpClientFactory instance is that we can configure multiple HttpClient instances depending on what types of endpoints are called.

For example, if we are calling an origin that requires an Authorization header added to it, we can define that in Program.cs by giving it a name and adding the appropriate value to the header.

When we create the HttpClient instance from the IHttpClientFactory instance, we can pass in that name and it will know to add the Authorization header to the request.

How to add to dependency injection

To test this out, we are going to use dummy endpoints from DummyJSON. So we can configure a HttpClient instance to call https://dummyjson.com/ from the BaseAddress.

In Program.cs, we call the AddHttpClient extension method from the IServiceCollection instance, passing in a name and the HttpClient instance. This will allow us to use IHttpClientFactory across our application.

We then use the HttpClient instance to set the BaseAddress.

// Program.cs
using RoundTheCode.HttpRequest.Service;

var builder = WebApplication.CreateBuilder(args);

...

builder.Services.AddHttpClient("DummyJSON", (httpClient) =>
{
	httpClient.BaseAddress = new Uri("https://dummyjson.com");
});

var app = builder.Build();

...

app.Run();

Use IHttpClientFactory to make a HTTP call

Now that we've added the IHttpClientFactory, we can inject it into our service.

We call the CreateClient method from it, passing in the name that we configured in Program.cs. In this instance, we would pass in DummyJSON.

As we have already configured the BaseAddress for this HttpClient instance in Program.cs, we can pass in a relative URL of the endpoint we wish to call.

// HttpService.cs
public class HttpService : IHttpService
{
	private readonly IHttpClientFactory _httpClientFactory;

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

	public async Task<string> ReadAsync()
	{
		using var httpClient = _httpClientFactory.CreateClient("DummyJSON");
		
		var response = await httpClient.GetAsync($"/products");

		if (!(response?.IsSuccessStatusCode ?? false))
		{
			return string.Empty;
		}

		return await response.Content.ReadAsStringAsync();			
	}
}

Binding the response to an object

Now that we've established the correct way of using HttpClient, we can now work out how to we want to bind a response.

When calling the /products endpoint in DummyJSON, we get a JSON response that is similar to this:

{
    "products": [
        {
            "id": 1,
            "title": "iPhone 9",
            "description": "An apple mobile which is nothing like apple",
            "price": 549,
            "discountPercentage": 12.96,
            "rating": 4.69,
            "stock": 94,
            "brand": "Apple",
            "category": "smartphones",
            "thumbnail": "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg",
            "images": [
                "https://cdn.dummyjson.com/product-images/1/1.jpg",
                "https://cdn.dummyjson.com/product-images/1/2.jpg",
                "https://cdn.dummyjson.com/product-images/1/3.jpg",
                "https://cdn.dummyjson.com/product-images/1/4.jpg",
                "https://cdn.dummyjson.com/product-images/1/thumbnail.jpg"
            ]
        }
    ]
}

Therefore, we can create a ProductModel class based on the properties that we wish to use.

For this example, we are going to take the product's id, title, description and price and add them as properties in the ProductModel class.

// Product.cs
public class Product
{
	public int Id { get; init; }

	public string? Title { get; init; }

	public string? Description { get; init; }

	public decimal? Price { get; init; }
}

We also need to create a ProductResponseModel class that will store all the products.

public class ProductResponseModel
{
	public List<ProductModel> Products { get; init; }
}

Deserialising the JSON response

With our models set up, we have a few ways of how we can deseralise the JSON response to these objects.

We can replace the GetAsync method from the HttpClient class, and use GetFromJsonAsync instead. The GetFromJsonAsync method requires us to pass in our object as the generic type.

// IHttpService.cs
public interface IHttpService
{
	Task<ProductResponseModel?> ReadAsync();
}
// HttpService.cs
public class HttpService : IHttpService
{
	private readonly IHttpClientFactory _httpClientFactory;

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

	public async Task<ProductResponseModel?> ReadAsync()
	{
		using var httpClient = _httpClientFactory.CreateClient("DummyJSON");
		
		return await httpClient.GetFromJsonAsync<ProductResponseModel?>($"/products");			
	}
}

This is a light way of binding the JSON response to our object.

However, if the endpoint does not return a successful status code, it will throw a HttpRequestException. We will need to handle this exception otherwise our endpoint will always throw a 500 exception.

// HttpService.cs
public class HttpService : IHttpService
{
	private readonly IHttpClientFactory _httpClientFactory;

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

	public async Task<ProductResponseModel?> ReadAsync()
	{
		using var httpClient = _httpClientFactory.CreateClient("DummyJSON");
		
		try
		{
			return await httpClient.GetFromJsonAsync<ProductResponseModel?>($"/products");
		}
		catch (HttpRequestException)
		{
			return null;
		}			
	}
}

The HttpRequestException does allow us to get the HTTP status code if there is an error. But it doesn't allow us to get it if it is successful.

Getting the HTTP response

Another way we can do it is to call GetAsync from the HttpClient instance.

Once we've established that the response is successful, we can call ReadFromJsonAsync method from the HttpContent property in the response and pass in the object as the generic type.

This way, we have access to the status code alongside a number of other properties of the response, regardless of whether the response is successful or not.

// HttpService.cs
public class HttpService : IHttpService
{
	private readonly IHttpClientFactory _httpClientFactory;

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

	public async Task<ProductResponseModel?> ReadAsync()
	{
		using var httpClient = _httpClientFactory.CreateClient("DummyJSON");
		
		var response = await httpClient.GetAsync("/products");

		if (!(response?.IsSuccessStatusCode) ?? false)
		{
			return null;
		}

		return await response.Content.ReadFromJsonAsync<ProductResponseModel>();			
	}
}

Watch the video

Watch the video where we talk you through some of the different ways that you can send a GET request and why some ways are better than others.

We'll also show you how to bind a JSON response from an API response to an object.