Problemas comuns IHttpClientFactory de uso

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

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

Devido a uma natureza complexa da integração com DI, você pode resolver alguns problemas que podem ser difíceis de IHttpClientFactory detetar e solucionar problemas. 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 Scoped a vida

Você pode ter um problema se precisar acessar qualquer serviço com escopo, por exemplo, HttpContextou algum cache com escopo, de dentro do HttpMessageHandler. Os dados salvos lá podem "desaparecer", ou, ao contrário, "persistir" 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, e é uma limitação conhecida no IHttpClientFactory.

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

Como resultado desta limitação:

  • Quaisquer dados armazenados em cache "externamente" em um serviço com escopo não estarão disponíveis no HttpMessageHandler.
  • Todos os dados armazenados em cache "internamente" dentro das dependências de escopo podem ser observados a HttpMessageHandler partir de vários escopos DI de aplicativos (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 ) dentro HttpMessageHandler de instâncias ou suas dependências para evitar o vazamento de HttpContextinformações confidenciais.

❌ NÃO utilize cookies, pois os CookieContainer serão partilhados juntamente com o manipulador.

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

Para passar informações arbitrárias ao lado do HttpRequestMessage, você pode usar a HttpRequestMessage.Options propriedade.

✔️ CONSIDERE encapsular toda a lógica relacionada ao escopo (por exemplo, autenticação) em um separado DelegatingHandler que não seja criado pelo , e use-a IHttpClientFactorypara encapsular o IHttpClientFactorymanipulador -created.

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

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

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

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

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

❌ NÃO injete instâncias de cliente digitadas em Singleton serviços.

✔️ CONSIDERE solicitar um cliente em tempo hábil ou sempre que precisar de IHttpClientFactory um. Os clientes criados na fábrica são seguros para descartar.

HttpClient As instâncias criadas por IHttpClientFactory destinam-se a ser de curta duração.

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

  • O descarte de tais HttpClient instâncias criadas pela fábrica não levará ao esgotamento da tomada, pois seu descarte não desencadeia o HttpMessageHandlerdescarte do . IHttpClientFactory rastreia e elimina os recursos usados para criar HttpClient instâncias, especificamente as HttpMessageHandler instâncias, assim que sua vida útil expira e não HttpClient há mais uso.

Os clientes tipados também devem ser de curta duração , pois uma HttpClient instância é injetada no construtor, portanto, ela compartilhará o tempo de vida do cliente digitado.

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

HttpClient usa muitos soquetes

Mesmo se IHttpClientFactory for usado, ainda é possível resolver o problema de exaustão 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 forem iniciadas simultaneamente ao mesmo tempo, cada uma delas acabará acionando uma nova tentativa de conexão HTTP, porque 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 ao mesmo tempo sem especificar os limites.

✔️ CONSIDERE a configuração HttpClientHandler.MaxConnectionsPerServer (ou SocketsHttpHandler.MaxConnectionsPerServer, se você usá-lo como um manipulador primário) para um valor razoável. Observe que esses limites só se aplicam à 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.

Cliente digitado tem a injeção errada HttpClient

Pode haver várias situações em que é possível obter uma injeção inesperada HttpClient em um cliente digitado. Na maioria das vezes, a causa raiz estará em uma configuração errada, pois, pelo design DI, qualquer registro subsequente de um serviço substitui o anterior.

Clientes tipados usam clientes nomeados "sob o capô": adicionar um cliente digitado implicitamente registra e o vincula a um cliente nomeado. O nome do cliente, a menos que explicitamente fornecido, será definido como o nome do tipo de TClient. Este seria o primeiro do TClient,TImplementation par se AddHttpClient<TClient,TImplementation> sobrecargas são usadas.

Portanto, registrar um cliente digitado faz duas coisas separadas:

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

As duas afirmações seguintes 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 vínculo entre clientes digitados e nomeados pode ser quebrado.

Cliente digitado é registrado uma segunda vez

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

Se um cliente digitado HttpClientFactoryfor erroneamente registrado uma segunda vez como um serviço transitório simples, isso substituirá o registro adicionado pelo , quebrando o link para o cliente nomeado. Ele se manifestará como se a HttpClientconfiguração do 's fosse perdida, pois um não configurado HttpClient será injetado no cliente digitado.

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

Diferentes clientes tipados são registrados em uma interface comum

No caso de dois clientes tipados diferentes serem registrados em uma interface comum, ambos reutilizariam o mesmo cliente nomeado. Isso pode parecer que o primeiro cliente digitado recebendo o segundo cliente nomeado "erroneamente" injetado.

❌ NÃO registre vários clientes digitados 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 digitados, especificando o nome na AddHttpClient<T> chamada ou chamando AddTypedClient durante a configuração do cliente nomeado.

Ao projetar, registrar e configurar um cliente nomeado com o mesmo nome várias vezes apenas acrescenta as ações de configuração à lista de ações existentes. Esse comportamento de pode não ser óbvio, mas é a mesma abordagem usada pelo padrão Opções e APIs de HttpClientFactory configuração como Configure.

Isso é útil principalmente para configurações avançadas de manipulador, por exemplo, adicionando um manipulador personalizado a um cliente nomeado definido externamente ou simulando um manipulador primário para testes, mas também funciona para HttpClient configuração de instância. 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 digitado 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 name parâmetro 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 alcançada 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 deseja registrar os clientes na mesma interface, você pode 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"));

Consulte também