.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 resistencia estándar de Microsoft.Extensions.Http.Resilience lanza una excepción 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 lo estableces
HttpClient.Timeout 100 segundos TaskCanceledException, con TimeoutException como su InnerException (.NET 5 y versiones posteriores) HttpClient.Timeout
Tiempo de espera del intento de manejador estándar 10 segundos por intento Nada al principio: el gestor vuelve a intentar el intento AddStandardResilienceHandler(options => ...)
Tiempo de espera total del handler 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 menor de los dos. Establézcalo en Timeout.InfiniteTimeSpan para desactivarlo.

En .NET 5 y versiones posteriores, un tiempo de espera produce una excepción TaskCanceledException con un TimeoutException elemento dentro. 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, compruebe ex.InnerException is TimeoutException o compruebe si se cancela su propio token.

Controlador de resiliencia estándar

AddStandardResilienceHandler() encadena un limitador de velocidad, un tiempo de espera total, un reintento, un interruptor automático y un tiempo de espera de 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 toda la solicitud, 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 usa los valores predeterminados del controlador estándar llama a una API que tarda de 11 a 15 segundos por respuesta, la aplicación obtiene un TimeoutRejectedException después de 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 usa el controlador estándar, intercepte TimeoutRejectedException. Mira TaskCanceledException para HttpClient.Timeout.
  2. Distinga entre un tiempo de espera y una cancelación. Solo se considera TaskCanceledException un tiempo de espera cuando su excepción interna es TimeoutException. Cuando la persona que llama canceló, deténgase discretamente.
  3. Elija los tiempos de espera que se ajusten a la API. Si la API suele tardar más de 10 segundos, cambie los tiempos de espera del intento y el total en AddStandardResilienceHandler(options => ...).
  4. No vuelva a intentarlo POST o PATCH a menos que la API haga que sea seguro. El controlador estándar vuelve a intentar todos los métodos de forma predeterminada, incluido POST. Llame options.Retry.DisableForUnsafeHttpMethods() para excluir POST, PATCH, PUT, DELETE y CONNECT, o options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) para mantener el reintento idempotente PUT y DELETE.
  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 produzca un tiempo de espera mientras desarrolla, 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
Esperar a la producción Tiempos de espera agotados Todo, hasta que un usuario lo pulse
Simula la API en tus pruebas o deja que el agente de codificación escriba el simulacro catch Si tu bloque se ejecuta y el mock lanza la excepción correcta La configuración real HttpClient, los reintentos del manejador de resiliencia y la excepción que realmente lanza. 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 se puede ralentizar la API cuando quieras.
Intercepta el tráfico real de la aplicación y retrasa las respuestas Tu verdadero HttpClient, gestor de resiliencia y 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 reintenta y, después de 30 segundos, el código recibe un TimeoutRejectedException. Para probar HttpClient.Timeout en su lugar, establezca minMs por encima del tiempo de espera que configuró. Para obtener más información sobre la configuración, consulte Uso de Dev Proxy con aplicaciones de .NET. Para instalar Dev Proxy, consulte Configurar Dev Proxy.

Pasos siguientes

Consulte también