Compartilhar via


Problemas IHttpClientFactory comuns de uso

Neste artigo, você aprenderá sobre alguns dos problemas mais comuns ao usar IHttpClientFactory para criar instâncias de HttpClient.

IHttpClientFactory é uma maneira conveniente de definir várias configurações de HttpClient no contêiner de DI, configurar o registro em logs, definir estratégias de resiliência e muito mais. IHttpClientFactorytambém encapsula o gerenciamento de tempo de vida de instâncias e HttpMessageHandler para evitar problemas como esgotamento de soquete e perda de HttpClient alterações de DNS. Para uma visão geral sobre como usar IHttpClientFactory em um aplicativo .NET, consulte IHttpClientFactory com .NET.

A integração de IHttpClientFactory com a DI é algo complexo e talvez você precise lidar com alguns problemas difíceis de detectar e solucionar. Os cenários listados neste artigo também contêm recomendações, que você pode aplicar proativamente para evitar possíveis problemas.

HttpClient não respeita o ciclo de vida de Scoped

Você pode encontrar um problema se precisar acessar qualquer serviço no escopo, por exemplo, HttpContext, ou algum cache no escopo, de dentro de HttpMessageHandler. Os dados salvos lá podem "desaparecer" ou, ao contrário, "permanecer lá" quando não deveriam. Isso é causado pela incompatibilidade de escopo de DI (Injeção de dependência) entre o contexto do aplicativo e a instância do manipulador, o que já é uma limitação conhecida no IHttpClientFactory.

IHttpClientFactory cria um escopo de DI separado para cada instância HttpMessageHandler. Esses escopos do manipulador são diferentes dos escopos de contexto do aplicativo (por exemplo, escopo de solicitação de entrada de ASP.NET Core ou o escopo de DI manual criado pelo usuário); portanto, eles não compartilham instâncias de serviço no escopo.

Como resultado dessa limitação:

  • Todos os dados armazenados em cache "externamente" em um serviço no escopo não estarão disponíveis no HttpMessageHandler.
  • Todos os dados armazenados em cache "internamente" dentro do HttpMessageHandler ou de suas dependências no escopo podem ser observados em vários escopos de DI do aplicativo (por exemplo, de diferentes solicitações de entrada), pois eles podem compartilhar o mesmo manipulador.

Considere as seguintes recomendações para ajudar a aliviar essa limitação conhecida:

❌ NÃO armazene em cache nenhuma informação relacionada ao escopo (como dados de HttpContext) dentroHttpMessageHandler de instâncias ou suas dependências para evitar o vazamento de informações confidenciais.

❌ NÃO use cookies, pois CookieContainer será compartilhado com o manipulador.

✔️ CONSIDERE não armazenar as informações ou apenas passá-las pela instância HttpRequestMessage.

Para passar informações arbitrárias com o HttpRequestMessage, você pode usar a propriedade HttpRequestMessage.Options.

✔️ CONSIDERE encapsular toda a lógica relacionada ao escopo (por exemplo, autenticação) em um DelegatingHandler separado que não seja criado por IHttpClientFactory e use para encapsular o manipulador criador por IHttpClientFactory.

Para criar apenas um HttpMessageHandler sem HttpClient, chame IHttpMessageHandlerFactory.CreateHandler para qualquer cliente nomeado registrado. Nesse caso, você precisará criar uma instância de HttpClient usando o manipulador combinado. Você pode encontrar um exemplo totalmente executável para essa solução alternativa no GitHub.

Para mais informações, consulte a seção Escopos do manipulador de mensagens em IHttpClientFactory nas diretrizes de IHttpClientFactory.

HttpClient não respeita as alterações de DNS

Mesmo se IHttpClientFactory for usado, ainda será possível encontrar o problema de DNS obsoleto. Isso geralmente pode acontecer se uma instância de HttpClient for capturada em um serviço Singleton ou, em geral, armazenada em algum lugar por um período maior do que o HandlerLifetime especificado. HttpClient também será capturado se o respectivo cliente digitado for capturado por um singleton.

❌ NÃO armazene instâncias de cache HttpClient criadas por IHttpClientFactory por períodos prolongados.

❌ NÃO injete instâncias de clientes tipados em serviços Singleton.

✔️ CONSIDERE solicitar um cliente de IHttpClientFactory em tempo hábil ou sempre que precisar de um. É seguro descartar os clientes criados de fábrica.

As instâncias HttpClient criadas por IHttpClientFactory devem ser de curta duração.

  • Reciclar e recriar HttpMessageHandlers quando o tempo de vida deles expira é essencial para IHttpClientFactory garantir que os manipuladores reajam às alterações de DNS. HttpClient é vinculado a uma instância de manipulador específica no momento de sua criação, portanto, novas instâncias HttpClient devem ser solicitadas em tempo hábil para garantir que o cliente receberá o manipulador atualizado.

  • O descarte dessas instâncias HttpClient e criadas pelo item de fábrica não leva ao esgotamento do soquete, pois seu descarte não dispara o descarte de HttpMessageHandler. IHttpClientFactory rastreia e descarta os recursos usados para criar instâncias HttpClient, especificamente as instâncias HttpMessageHandler, assim que o tempo de vida delas expira e HttpClient não as utiliza mais.

Os clientes tipados também devem ter vida curta curta porque uma instância de HttpClient é injetada no construtor, portanto, ela compartilhará o tempo de vida do cliente tipado.

Para mais informações, consulte as seções de HttpClientgerenciamento de tempo de vida e Evitar clientes tipados em serviços de singleton nas diretrizes de IHttpClientFactory.

HttpClient usa muitos soquetes

Mesmo se IHttpClientFactory for usado, ainda é possível resolver o problema de esgotamento do soquete com um cenário de uso específico. Por padrão, HttpClient não limita o número de solicitações simultâneas. Se um grande número de solicitações HTTP/1.1 for iniciado simultaneamente ao mesmo tempo, cada uma delas acabará disparando uma nova tentativa de conexão HTTP, pois não há conexão livre no pool e nenhum limite é definido.

❌ NÃO inicie um grande número de solicitações HTTP/1.1 simultaneamente sem especificar limites.

✔️ CONSIDERE definir HttpClientHandler.MaxConnectionsPerServer (ou SocketsHttpHandler.MaxConnectionsPerServer, se você usar como um manipulador primário) com um valor razoável. Esses limites se aplicam apenas à instância específica do manipulador.

✔️ CONSIDERE o uso de HTTP/2, que permite solicitações de multiplexação em uma única conexão TCP.

O cliente tipado tem o HttpClient errado injetado

Pode haver várias situações em que é possível aparecer inesperadamente um HttpClient injetado em um cliente tipado. Na maioria das vezes, a causa raiz será uma configuração incorreta, pois, pelo design de DI, qualquer registro subsequente de um serviço substitui o anterior.

Os clientes tipados usam clientes nomeados de forma ocultada: adicionar um cliente tipado acaba registrando e vinculando implicitamente a um cliente nomeado. O nome do cliente, a menos que seja fornecido de forma explícita, será definido como o nome do tipo de TClient. Este seria o primeiro do par TClient,TImplementation se sobrecargas de AddHttpClient<TClient,TImplementation> forem usadas.

Portanto, registrar um cliente tipado faz duas coisas separadas:

  1. Registra um cliente nomeado (em um caso padrão simples, o nome é typeof(TClient).Name).
  2. Registra um serviço Transient usando o TClient ou o TClient,TImplementation fornecido.

As duas instruções a seguir são tecnicamente as mesmas:

services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));

// -OR-

services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
    .AddTypedClient<ExampleClient>(); // link the named client to a typed client

Em um caso simples, também será semelhante ao seguinte:

services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client

// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
    new ExampleClient(
        s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));

Considere os exemplos a seguir de como o link entre clientes tipados e nomeados pode acabar quebrado.

O cliente tipado é registrado uma segunda vez

❌NÃO registre o cliente tipado separadamente — ele já está registrado automaticamente pela chamada AddHttpClient<T>.

Se um cliente tipado for registrado por engano uma segunda vez como um serviço transitório simples, isso substituirá o registro adicionado pelo HttpClientFactory, quebrando o link para o cliente nomeado. Ele se manifestará como se a configuração de HttpClient fosse perdida, pois um HttpClient sem configuração será injetado no cliente tipado.

Talvez pareça confuso que, em vez de lançar uma exceção, um HttpClient "errado" seja usado. Isso acontece porque o HttpClient "padrão" não configurado — o cliente com o nome Options.DefaultName (string.Empty) — é registrado como um serviço transitório simples para habilitar o cenário de uso de HttpClientFactory mais básico. É por isso que, depois que o link quebra e o cliente tipado se torna apenas um serviço comum, esse HttpClient "padrão" será naturalmente injetado no respectivo parâmetro do construtor.

Clientes tipados são registrados em uma interface comum

Caso dois clientes tipados fossem registrados em uma interface comum, ambos reutilizariam o mesmo cliente nomeado. Isso pode parecer que o primeiro cliente tipado está recebendo o segundo cliente nomeado injetado "erroneamente".

❌ NÃO registre vários clientes tipados em uma única interface sem especificar explicitamente o nome.

✔️ CONSIDERE registrar e configurar um cliente nomeado separadamente e, em seguida, vinculá-lo a um ou vários clientes tipados especificando o nome na chamada AddHttpClient<T> ou chamando AddTypedClient durante a configuração do cliente nomeado.

Por design, registrar e configurar um cliente nomeado com o mesmo nome várias vezes apenas anexa as ações de configuração à lista do que já existe. Esse comportamento de HttpClientFactory pode não ser óbvio, mas é a mesma abordagem usada pelo padrão Options e pelas APIs de configuração, como Configure.

Isso é útil principalmente para configurações avançadas de manipulador, por exemplo, adicionar um manipulador personalizado a um cliente nomeado definido externamente ou simular um manipulador primário para testes, mas também funciona para a configuração da instância de HttpClient. Por exemplo, os três exemplos a seguir resultarão em um HttpClient configurado da mesma maneira (ambos BaseAddress e DefaultRequestHeaders são definidos):

// one configuration callback
services.AddHttpClient("example", c =>
    {
        c.BaseAddress = new Uri("http://example.com");
        c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
    });

// -OR-

// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
    .ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));

// -OR-

// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
    .ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));

Isso permite vincular um cliente tipado tipado a um cliente nomeado já definido e também vincular vários clientes tipados a um único cliente nomeado. É mais óbvio quando sobrecargas com um parâmetro name são usadas:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));

services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");

A mesma coisa também pode ser obtida chamando AddTypedClient durante a configuração do cliente nomeado:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
    .AddTypedClient<FooLogger>()
    .AddTypedClient<BarLogger>();

No entanto, se você não quiser reutilizar o mesmo cliente nomeado, mas ainda desejar registrar os clientes na mesma interface, poderá fazê-lo especificando explicitamente nomes diferentes para eles:

services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
    c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
    c => c.BaseAddress = new Uri("https://github.com"));

Confira também