Compartir a través de


Problemas de uso IHttpClientFactory comunes

En este artículo, conocerá algunos de los problemas más comunes que le pueden surgir al usar IHttpClientFactory para crear instancias de HttpClient.

IHttpClientFactory es una manera cómoda de configurar varias configuraciones de HttpClient en el contenedor de inserción de dependencias, configurar el registro, preparar las estrategias de resistencia, etc. IHttpClientFactory también encapsula la administración del ciclo de vida de las instancias de HttpClient y HttpMessageHandler para evitar problemas como el agotamiento de sockets y la pérdida de cambios de DNS. Para obtener información general sobre cómo usar IHttpClientFactory en la aplicación .NET, consulte IHttpClientFactory con .NET.

Debido a la naturaleza compleja de la integración de IHttpClientFactory con la inserción de dependencias, le pueden surgir algunos problemas que podrían ser difíciles de detectar y solucionar. Los escenarios que figuran en este artículo también incluyen recomendaciones, que puede aplicar de forma proactiva para evitar posibles problemas.

HttpClient no respeta la duración de Scoped

Puede producirse un problema si necesita acceder a cualquier servicio con ámbito, por ejemplo, HttpContext o alguna caché con ámbito, dentro de HttpMessageHandler. Ahí, los datos guardados pueden "desaparecer" o, al revés, "persistir" cuando no deberían hacerlo. Esto se debe a que el ámbito de la inserción de dependencias (DI) no es el mismo en el contexto de la aplicación y la instancia del controlador y es una limitación conocida en IHttpClientFactory.

IHttpClientFactory crea un ámbito de inserción de dependencias aparte por cada instancia de HttpMessageHandler. Estos ámbitos de controladores se distinguen de los del contexto de la aplicación (por ejemplo, un ámbito de solicitudes entrantes de ASP.NET Core o un ámbito de inserción de dependencias manual creado por el usuario), por lo que no compartirán instancias de servicio con ámbito.

Como consecuencia de esta limitación, pasa lo siguiente:

  • Los datos almacenados en caché "externamente" en un servicio con ámbito no estarán disponibles en el HttpMessageHandler.
  • Los datos almacenados "internamente" en caché dentro del HttpMessageHandler o sus dependencias con ámbito sí se pueden observar a través de varios ámbitos de inserción de dependencias de la aplicación (por ejemplo, mediante diferentes solicitudes entrantes) que pueden compartir el mismo controlador.

Tenga en cuenta las siguientes recomendaciones para mitigar esta limitación conocida:

❌ NO almacene en caché ninguna información relacionada con el ámbito (como datos de HttpContext) dentro de las instancias de HttpMessageHandler o sus dependencias para evitar que se filtre información confidencial.

❌ NO use cookies, ya que el CookieContainer se compartirá junto con el controlador.

✔️ SE RECOMIENDA no almacenar la información o pasarla solo a la instancia de HttpRequestMessage.

Para pasar información arbitraria junto con el HttpRequestMessage, puede usar la propiedad HttpRequestMessage.Options.

✔️ SE RECOMIENDA encapsular toda la lógica relacionada con el ámbito (por ejemplo, la autenticación) en un DelegatingHandler independiente que no lo crea el IHttpClientFactory y úselo para encapsular el controlador de IHttpClientFactory creado.

Para crear solo un HttpMessageHandler sin HttpClient, llame a IHttpMessageHandlerFactory.CreateHandler en cualquier cliente con nombre registrado. En ese caso, creará una instancia de HttpClient mediante el controlador combinado. Puede consultar un ejemplo totalmente ejecutable de esta solución en GitHub.

Para obtener más información, consulte la sección Ámbitos de controladores de mensajes en IHttpClientFactory en las instrucciones sobre IHttpClientFactory.

HttpClient no respeta los cambios de DNS

Aunque se use IHttpClientFactory, puede seguir apareciendo el problema de DNS obsoleto. Esto suele ocurrir si una instancia de HttpClient se captura en un servicio de Singleton o, en general, se almacena en algún lugar durante un período de tiempo mayor que el HandlerLifetime indicado. También se capturará HttpClient si un el respectivo cliente con tipo lo captura un singleton.

❌ NO almacene en caché las instancias de HttpClient creadas por IHttpClientFactory durante largos períodos de tiempo.

❌ NO inserte instancias de clientes con tipo en servicios de Singleton.

✔️ SE RECOMIENDA solicitar un cliente de IHttpClientFactory en el momento correspondiente o cada vez que necesite uno. Los clientes creados por la fábrica son seguros de eliminar.

Las instancias de HttpClient creadas por IHttpClientFactory están pensadas para ser de corta duración.

  • El reciclaje y la recreación de las instancias de HttpMessageHandler cuando expira su duración es esencial para que IHttpClientFactory se asegure de que los controladores reaccionan a los cambios de DNS. HttpClient se vincula a una instancia de controlador específica cuando se crea, por lo que deben solicitarse nuevas instancias de HttpClient a tiempo para asegurarse de que el cliente obtenga el controlador actualizado.

  • Si se eliminan estas instancias de HttpClientcreadas por la fábrica no provocará el agotamiento de sockets, ya que su eliminación no hará que se elimine HttpMessageHandler. IHttpClientFactory hace un seguimiento y elimina los recursos que se usan para crear instancias de HttpClient, específicamente las instancias de HttpMessageHandler, en cuanto expira su duración y ya no hay ningún HttpClient usándolas.

Se supone que los clientes con tipo son de corta duración y, además, como cuando una instancia de HttpClient se inserta en el constructor, lo mismo pasará con la duración del cliente con tipo.

Para obtener más información, consulte las secciones sobre la HttpClient administración de la duración y cómo evitar clientes con tipo en servicios de singleton en las instrucciones de IHttpClientFactory.

HttpClient usa demasiados sockets

Aunque se use IHttpClientFactory , aún es posible que surja el problema de agotamiento de sockets en un caso concreto. De forma predeterminada, HttpClient no pone un límite el número de solicitudes simultáneas. Si se inicia un número elevado de solicitudes de HTTP/1.1 al mismo tiempo, cada una de ellas terminará conllevando un nuevo intento de conexión HTTP, ya que no hay ninguna conexión gratuita en el grupo y no se establece ningún límite.

❌ NO inicie un elevado número de solicitudes HTTP/1.1 simultáneamente al mismo tiempo sin indicar los límites.

✔️ ES RECOMENDABLE que en HttpClientHandler.MaxConnectionsPerServer (o en SocketsHttpHandler.MaxConnectionsPerServer si lo usa como controlador principal), se elija un valor razonable. Tenga en cuenta que estos límites solo se aplican a la instancia del controlador específica.

✔️ SE RECOMIENDA usar HTTP/2, ya que permite la multiplexación de solicitudes a través de una única conexión TCP.

El cliente con tipo tiene la inserción HttpClient incorrecta

Puede haber varias situaciones en las que sea posible insertar una inserción HttpClient inesperada en un cliente con tipo. La mayoría de veces, la causa principal será una configuración errónea, ya que, por el diseño de la inserción de dependencias, cualquier registro posterior de un servicio invalida al anterior.

Los clientes con tipo usan clientes con nombre "en segundo plano": al agregar un cliente con tipo se registra implícitamente y se vincula con un cliente con nombre. El nombre de cliente, a menos que se indique explícitamente, se aplicará como el nombre de tipo de TClient. Este sería el primero del par de TClient,TImplementation si se usan sobrecargas de AddHttpClient<TClient,TImplementation>.

Por ello, el registro de un cliente con tipo implica dos acciones independientes:

  1. Registra un cliente con nombre (en un caso predeterminado normal, el nombre es typeof(TClient).Name).
  2. Registra un servicio de Transient a través del TClient o del TClient,TImplementation facilitado.

Las dos declaraciones siguientes son técnicamente iguales:

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

En un caso sencillo, también será similar al siguiente:

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))));

Fíjese en los ejemplos siguientes para saber cómo se puede romper el vínculo entre clientes con tipo y clientes con nombre.

El cliente con tipo se registra una segunda vez

❌ NO registre el cliente con tipo por separado; ya se registra automáticamente mediante la llamada a AddHttpClient<T>.

Si un cliente con tipo se registra erróneamente una segunda vez como servicio transitorio sin formato, esto sobrescribirá el registro agregado por la HttpClientFactory, lo que interrumpirá el vínculo con el cliente con nombre. Se manifestará como si se perdiera la configuración de HttpClient, ya que el HttpClient no configurado se insertará en el cliente con tipo en su lugar.

Puede resultar confuso que, en lugar de generarse una excepción, se use un HttpClient "incorrecto". Esto sucede porque el "valor predeterminado" HttpClient no configurado (el cliente con el nombre Options.DefaultName (string.Empty) se registra como un servicio transitorio sin formato para habilitar el contexto de uso de HttpClientFactory más básico. Es por eso que después de que el vínculo se interrumpe y el cliente con tipo se convierte solo en un servicio normal, este "valor predeterminado" HttpClient se insertará de forma natural en el parámetro del constructor respectivo.

Los distintos clientes con tipo se registran en una interfaz común

En caso de que dos clientes con tipo diferentes se registren en una interfaz común, ambos reutilizarían el mismo cliente con nombre. Esto puede dar a entender que el primer cliente con tipo hace que se inserte el segundo cliente con nombre "erróneamente".

❌ NO registre varios clientes con tipo en una sola interfaz sin indicar explícitamente el nombre.

✔️ SE RECOMIENDA registrar y configurar un cliente con nombre por separado y luego vincularlo a uno o varios clientes con tipo, ya sea indicando el nombre en la llamada a AddHttpClient<T> o llamando a AddTypedClient durante la configuración del cliente con nombre.

Por diseño, el registro y la configuración de un cliente con nombre con el mismo nombre varias veces incorpora las acciones de configuración a la lista de las que ya existen. Esta modo de uso de HttpClientFactory podría no ser obvio, pero es el mismo método que usa el Patrón de opciones y las API de configuración, como Configure.

Esto es básicamente útil para las configuraciones avanzadas de controladores, por ejemplo, agregar un controlador personalizado a un cliente con nombre definido externamente o simular un controlador principal para pruebas, aunque también funciona para la configuración de la instancia de HttpClient. Por ejemplo, los tres ejemplos siguientes darán como resultado un HttpClient configurado de la misma manera (se crea tanto BaseAddress como DefaultRequestHeaders):

// 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"));

Esto permite vincular un cliente con tipo con un cliente con nombre ya definido y, además, vincular varios clientes con tipo a un único cliente con nombre. Es más evidente cuando se usan sobrecargas con un parámetro name:

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

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

También se puede lograr lo mismo llamando a AddTypedClient durante la configuración del cliente con nombre:

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

Sin embargo, si no desea reutilizar el mismo cliente con nombre, pero desea registrar los clientes en la misma interfaz, puede hacerlo indicando explícitamente los diferentes nombres para ellos:

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 también