- Home
- .NET tutorials
- How to use HttpClient correctly to avoid socket exceptions
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.
C# coding challenges
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.