.NET HttpClient: tiempos de espera, TaskCanceledException y TimeoutRejectedException

Cuando una API es demasiado lenta, la aplicación de .NET obtiene 1 de 2 excepciones, en función del tiempo de espera que se desencadene. HttpClient.Timeout lanza una TaskCanceledExceptionexcepción. El controlador de resiliencia estándar de Microsoft.Extensions.Http.Resilience lanza un TimeoutRejectedException de Polly. Proceden de diferentes lugares, tienen valores predeterminados diferentes y necesitan bloques independientes catch .

¿Qué timeout se activó?

Tiempo de espera Predeterminado Qué obtiene tu código Dónde se configura
HttpClient.Timeout 100 segundos TaskCanceledException, con TimeoutException como su InnerException (.NET 5 y versiones posteriores) HttpClient.Timeout
Tiempo de espera de intento del controlador estándar 10 segundos por intento Nada al principio: el controlador vuelve a intentar la operación AddStandardResilienceHandler(options => ...)
Tiempo de espera total del controlador estándar 30 segundos, incluidos todos los reintentos Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout se aplica a todas las solicitudes que envía la HttpClient instancia. Para usar un tiempo de espera diferente para una solicitud, pase un CancellationToken desde un CancellationTokenSource con su propio tiempo de espera. Se aplica el más corto de los dos. Configura Timeout.InfiniteTimeSpan para desactivarlo.

En .NET 5 y versiones posteriores, un tiempo de espera lanza una TaskCanceledException con un TimeoutException en su interior. En versiones anteriores de .NET Core, la excepción interna no existe. En .NET Framework, obtendrá HttpRequestException en su lugar. Para obtener más información, consulte HttpClient.Timeout y Realizar solicitudes HTTP con la clase HttpClient.

También TaskCanceledException significa que alguien canceló la solicitud, por ejemplo, un usuario que cerró la página. Para distinguir un tiempo de espera de una cancelación, comprueba ex.InnerException is TimeoutException o comprueba si tu propio token ha sido cancelado.

El controlador de resiliencia estándar

AddStandardResilienceHandler() combina un limitador de velocidad, un tiempo de espera total, un reintento, un cortacircuitos y un tiempo de espera por intento. Cuando un intento tarda más de 10 segundos, el tiempo de espera del intento lo cancela y la estrategia de reintento vuelve a intentarlo: hasta 3 reintentos, con retroceso exponencial y variación aleatoria, a partir de 2 segundos. Cuando la solicitud completa, incluidos los reintentos, tarda más de 30 segundos, el tiempo de espera total la cancela y tu código recibe un TimeoutRejectedException.

TimeoutRejectedException deriva de Exception. No es ni un TimeoutException ni un HttpRequestException, por lo que un bloque catch (HttpRequestException) no lo captura. Para obtener la lista completa de valores predeterminados, consulte Valores predeterminados del controlador de resistencia estándar.

Por ejemplo, cuando una aplicación de .NET 10 que utiliza los valores predeterminados del controlador estándar llama a una API que tarda entre 11 y 15 segundos por respuesta, la aplicación recibe un TimeoutRejectedException tras 30 segundos y su bloque catch (HttpRequestException) no se ejecuta.

Cómo controlar los tiempos de espera de HttpClient

  1. Captura ambas excepciones donde llames a la API. Si utilizas el controlador estándar, captura TimeoutRejectedException. Captura TaskCanceledException para HttpClient.Timeout.
  2. Distingue entre un tiempo de espera y una cancelación. Solo trata un TaskCanceledException como un tiempo de espera cuando su excepción interna sea un TimeoutException. Cuando el llamante haya cancelado, detente sin generar avisos.
  3. Elija los tiempos de espera que se ajusten a la API. Si la API suele tardar más de 10 segundos, cambia el tiempo de espera por intento y el tiempo de espera total en AddStandardResilienceHandler(options => ...).
  4. No reintentes POST ni PATCH a menos que la API lo permita de forma segura. El controlador estándar vuelve a intentar todos los métodos de forma predeterminada, incluido POST. Llama a options.Retry.DisableForUnsafeHttpMethods() para excluir POST, PATCH, PUT, DELETE y CONNECT, o a options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) para seguir reintentando PUT e DELETE, que son idempotentes.
  5. Indique al usuario qué ha ocurrido. Muestra "el servicio es lento, inténtelo de nuevo" en lugar de un error genérico.
using Polly.Timeout;

public async Task<string?> GetForecastAsync(HttpClient client, CancellationToken cancellationToken)
{
    try
    {
        return await client.GetStringAsync("https://api.contoso.com/forecast", cancellationToken);
    }
    catch (TimeoutRejectedException)
    {
        // Standard resilience handler: total timeout expired after all retries
        return null;
    }
    catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
    {
        // HttpClient.Timeout expired
        return null;
    }
    catch (HttpRequestException)
    {
        // Network error, or an error status code after all retries
        return null;
    }
}

Cómo probar que la aplicación controla los tiempos de espera

Rara vez se produce un tiempo de espera durante el desarrollo, por lo que los bloques catch rara vez se ejecutan. La forma de probar decide si encuentras los errores antes de que los usuarios lo hagan.

Approach Lo que encuentras Lo que te pierdes
A la espera de la producción Tiempos de espera reales Todo, hasta que un usuario lo pulse
Simula la API en tus pruebas o deja que tu agente de programación escriba la simulación Independientemente de si se ejecuta el bloque catch, si la simulación lanza la excepción correcta Tu configuración real de HttpClient, los reintentos del gestor de resiliencia y la excepción que lanza realmente. La aplicación también necesita un interruptor solo para pruebas para llegar al mock.
Llame a la API real y espere que sea lenta. Comportamiento real No puedes ralentizar la API a voluntad
Intercepta el tráfico real de tu aplicación y retrasa las respuestas Tu HttpClient real, el gestor de resiliencia y las excepciones Nada en la aplicación cambia, por lo que no prueba el código de forma aislada. Reserve las pruebas unitarias para eso.

Pruébalo en tu app

Dev Proxy intercepta las solicitudes de la aplicación a la API y retrasa las respuestas con LatencyPlugin. .NET usa el proxy del sistema, por lo que no es necesario cambiar el código. En este ejemplo se retrasa cada respuesta de 11 a 15 segundos, más que el tiempo de espera de intento de 10 segundos del controlador estándar. Reemplace https://api.contoso.com por la dirección URL de la API a la que llama la aplicación.

Archivo: devproxyrc.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/rc.schema.json",
  "plugins": [
    {
      "name": "LatencyPlugin",
      "enabled": true,
      "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
      "configSection": "slowApi"
    }
  ],
  "urlsToWatch": [
    "https://api.contoso.com/*"
  ],
  "slowApi": {
    "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/latencyplugin.schema.json",
    "minMs": 11000,
    "maxMs": 15000
  }
}

Inicie el proxy de desarrollo con devproxy --config-file devproxyrc.json y ejecute la aplicación. Cada intento agota el tiempo de espera, el controlador vuelve a intentarlo y, tras 30 segundos, tu código recibe un TimeoutRejectedException. Para probar HttpClient.Timeout en su lugar, establece minMs en un valor superior al tiempo de espera que hayas configurado. Para obtener detalles sobre la configuración, consulta Usar Dev Proxy con aplicaciones .NET. Para instalar Dev Proxy, consulte Configurar Dev Proxy.

Pasos siguientes

Consulte también