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. IHttpClientFactory
também encapsula o gerenciamento de tempo de vida de instâncias HttpClient
e HttpMessageHandler
para evitar problemas como esgotamento de soquete e perda de 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
HttpMessageHandler
s quando o tempo de vida deles expira é essencial paraIHttpClientFactory
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ânciasHttpClient
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 deHttpMessageHandler
.IHttpClientFactory
rastreia e descarta os recursos usados para criar instânciasHttpClient
, especificamente as instânciasHttpMessageHandler
, assim que o tempo de vida delas expira eHttpClient
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 HttpClient
gerenciamento 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:
- Registra um cliente nomeado (em um caso padrão simples, o nome é
typeof(TClient).Name
). - Registra um serviço
Transient
usando oTClient
ou oTClient,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"));