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 queIHttpClientFactory
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 deHttpClient
a tiempo para asegurarse de que el cliente obtenga el controlador actualizado.Si se eliminan estas instancias de
HttpClient
creadas por la fábrica no provocará el agotamiento de sockets, ya que su eliminación no hará que se elimineHttpMessageHandler
.IHttpClientFactory
hace un seguimiento y elimina los recursos que se usan para crear instancias deHttpClient
, específicamente las instancias deHttpMessageHandler
, en cuanto expira su duración y ya no hay ningúnHttpClient
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:
- Registra un cliente con nombre (en un caso predeterminado normal, el nombre es
typeof(TClient).Name
). - Registra un servicio de
Transient
a través delTClient
o delTClient,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"));