C# - Você sabe mesmo utilizar o HttpClient corretamente?

Quando precisamos fazer uma requisição HTTP em C#, tudo parece muito simples. Basta usar o HttpClient, certo? E a primeira implementação que a maioria faz é algo assim:

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("accept", "application/json");
var response = await client.GetAsync($"{host}:{port}/{path}");
var responseContent = await response.Content.ReadAsStringAsync();
Console.WriteLine(responseContent);

Com isso, achamos que está tudo resolvido e marcamos a tarefa como concluída. Funcionou, e se funcionou está tudo certo… será mesmo?

Bom, é aí que você se engana, meu caro adepto do CTRL+C e CTRL+V. Neste artigo, vou explicar os problemas dessa abordagem, focando no lado do cliente e como evitá-los para não se deparar com bugs difíceis de identificar.

Entendendo o problema

Antes de falar da solução, é importante entender como uma requisição HTTP funciona. De forma bem simplificada:

Toda requisição HTTP é feita usando sockets. Para abrir um socket, você precisa de um IP e uma PORTA. O cliente se conecta ao servidor, que está ouvindo na porta configurada, e após um “handshake” bem-sucedido (basicamente um “oi, vamos trocar pacotes?”), um socket é criado no cliente e outro no servidor. Esse socket fica ativo até o tempo configurado no Keep-Alive ou até ser encerrado.

Agora, qual o problema de criar um HttpClient novo a cada requisição?

A cada nova instância de HttpClient, o sistema é forçado a abrir um novo socket. Mesmo quando você acessa o mesmo servidor (IP e porta), isso resulta em um consumo excessivo de recursos tanto no cliente quanto no servidor.

Em cenários com alto volume de requisições, isso pode esgotar o limite de sockets do sistema operacional. Resultado? System.Net.Sockets.SocketException. E se você já enfrentou isso em produção, sabe que é um pesadelo identificar o que está causando essa exceção.

Como identificar o problema

Para visualizar o comportamento errado, você pode usar o comando tcpdump e observar as conexões sendo abertas:

15:21:49.112706 lo    In  IP localhost.5000 > localhost.54450: Flags [S.], seq 2951581501, ack 4180718991, win 65483, options [mss 65495,sackOK,TS val 2948923132 ecr 2948923132,nop,wscale 7], length 0

15:21:49.488534 lo    In  IP localhost.5000 > localhost.54458: Flags [S.], seq 3305744583, ack 3367141433, win 65483, options [mss 65495,sackOK,TS val 2948923508 ecr 2948923508,nop,wscale 7], length 0

Observe no resultado acima que foram criados dois sockets, um para cada requisição, mesmo que seja para o mesmo servidor. Esse comportamento é o principal sinal de que algo está errado.

A solução

A solução é reutilizar os sockets existentes sempre que possível. Isso pode ser feito de duas formas:

Usando uma instância estática de HttpClient para toda a aplicação. Utilizando o IHttpClientFactory para gerenciar as instâncias de HttpClient via injeção de dependência.

1. Instância estática de HttpClient

Essa abordagem é simples e funciona bem em cenários onde você precisa se conectar a poucos servidores. Exemplo:

public class HttpClientStaticService
{
    private static readonly HttpClient Client = new HttpClient();
    
    public async Task Request(string host, int port, string path)
    {
       ...
            var response = await Client.GetAsync($"{host}:{port}/{path}");    
       ...
    }
}

2. IHttpClientFactory

Se sua aplicação precisa lidar com múltiplos servidores ou configurações específicas para cada requisição, o IHttpClientFactory é a melhor escolha. Ele permite criar instâncias personalizadas, e ainda reutiliza os sockets. Exemplo:

public class HttpClientFactoryService
{
    private readonly HttpClient _httpClient;

    public HttpClientFactoryService(HttpClient httpClient)
        => _httpClient = httpClient;

    public async Task GetWeatherForecastAsync()
    {
        ...
            var response = await _httpClient.GetAsync("weatherforecast");
        ...
    }
}

Qual abordagem usar?

Depende do seu caso:

Instância estática: Ideal para aplicações simples onde poucas configurações são necessárias. IHttpClientFactory: Melhor para cenários mais complexos, onde múltiplas configurações são necessárias.

Conclusão

Saber como funciona o HttpClient e a reutilização de sockets não é só uma questão de performance, mas também para evitar erros difíceis de diagnosticar em produção. Escolher a melhor abordagem para o seu projeto evitará dores de cabeça no futuro.

Ah, e só para esclarecer: o que realmente se esgota não é o número de sockets, mas sim o limite de file descriptors (no caso de sistemas Unix/Linux). Mas isso é papo para outro artigo.

Confira o código no GitHub.

Se você gostou do conteúdo, curta e compartilhe sua opinião nos comentários! Fique à vontade para adicionar qualquer ponto ou sugestão que achar relevante.

Até mais, e fique com Deus!

🔗 Confira o código no GitHub.

🔗 Link para a postagem no Linkedin