[.NET] ¡No hagan Dispose() en HttpClient!

Cuando desarrollamos un sistema conectado en .NET es probable que lo terminemos haciendo con HttpClient, esto es por todas las ventajas que nos trae:

  1. Fue diseñado especialmente para la creciente necesidad de las solicitudes bajo una arquitectura REST y Henrik F. Nielson, el autor inicial de HTTP, esta involucrado en su desarrollo y diseño.
  2. Puede ser usado en aplicaciones de escritorio para Windows, Linux o Mac, así como para aplicaciones móviles en Android o iOS y aplicaciones de IoT con Windows 10.
  3. Puede reusar DNS resueltos y te permite configurar facilmente headers y cookies.
  4. Una única instancia de HttpClient puede hacer solicitudes concurrentes.
  5. La API es sencilla y eso es genial para hacer pruebas.
  6. Todas las operaciones de espera son asíncronas.

Y otras ventajas más, pero el punto de este post esta en relación al error en la que solemos caer algunos cuando nos damos cuenta que HttpClient implementa la interfaz IDisposable.

¿Qué es lo que sucede?

En el siguiente ejemplo tenemos un programa que crea una nueva instancia de HttpClient por cada solicitud que se realiza.

Y al ejecutarlo, si bien las operaciones culminan con aparente normalidad, la verdad es que dejan varias conexiones abiertas.

Esto es porque las conexiones sobre TCP/IP no se cierran en su totalidad para permitir que los paquetes lleguen fuera de orden o que se vuelvan a transmitir. TIME_WAIT indica que el punto final de nuestro lado ha cerrado las conexiones, pero las conexiones se mantienen para que los paquetes retrasados puedan manejarse correctamente. Una vez que esto sucede, las conexiones se eliminarán luego de un período de tiempo de espera de 4 minutos.

Nuestro programa pudo haber abierto hasta 10 nuevos sockets hacia el servidor, pero ¿te imaginas qué podría pasar en el backend de algún proyecto? Simplemente nos veremos frente a un error similar este:

Unable to connect to the remote server
System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted

¿Qué podemos hacer?

La mejor solución es reusar una única instancia de nuestro HttpClient en vez de crear una para cada solicitud. Y no hay de qué preocuparse porque casi todas las operaciones son thread-safe: CancelPendingRequests, DeleteAsync, GetAsync, GetByteArrayAsync, GetStreamAsync, GetStringAsync, PostAsync, PutAsync y SendAsync.

Ahora, al reutilizar una instancia de HttpClient significa que se mantiene el socket hasta que se cierre, por lo que si se tiene una actualización de registro DNS en el servidor, el cliente nunca lo sabrá hasta que se cierre ese socket. Y esto puede ser otro gran problema, ¿qué pasaría si para nuestro backend hacemos un switch de IPs para pasar una actualización del proyecto a producción? ¡El cliente todavía estaría conectándose a la instancia anterior, osea a la versión anterior del proyecto!

HttpClient no verifica los registros DNS cuando la conexión está abierta y tiene sentido. Por eso tenemos que ir un poco más allá y establecer el valor por defecto de por cuánto tiempo el socket TCP, para un endpoint determinado, puede permanecer abierto.

Pero eso no es todo, una vez cerrada la conexión, nada te asegura de que al momento de que el HttpClient establezca una nueva conexión usando el mismo DNS, resuelva y se conecte a la nueva IP del servidor y esto ocurre porque la resolución del DNS se guarda en memoria. Por ello es que finalmente, también tenemos que hacer lo siguiente.

Agregue un comentario

Su dirección de correo no se hará público.