.NET délais d’expiration HttpClient : TaskCanceledException et TimeoutRejectedException

Lorsqu’une API est trop lente, votre application .NET reçoit l’une des deux exceptions, selon le délai d’expiration déclenché. HttpClient.Timeout génère une TaskCanceledException. Le gestionnaire de résilience standard lève un TimeoutRejectedException de Microsoft.Extensions.Http.Resilience Polly. Ils proviennent de différents endroits, ont des valeurs par défaut différentes et ont besoin de blocs distincts catch .

Quel timeout s’est déclenché

Délai d'expiration Default Ce que votre code obtient L’endroit où vous l’avez réglé
HttpClient.Timeout 100 secondes TaskCanceledException, avec un TimeoutException comme son InnerException (.NET 5 et versions ultérieures) HttpClient.Timeout
Délai d’expiration de la tentative de gestionnaire standard 10 secondes par tentative Rien au début : le gestionnaire réessaie AddStandardResilienceHandler(options => ...)
Délai d’expiration total du gestionnaire standard 30 secondes, y compris tous les réessais Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout s’applique à chaque requête envoyée par l’instance HttpClient . Pour utiliser un délai d’expiration différent pour une requête, transmettez un CancellationToken provenant d’un CancellationTokenSource avec son propre délai d’expiration. Le plus court des deux s’applique. Définissez cette option sur Timeout.InfiniteTimeSpan pour la désactiver.

Sur .NET 5 et versions ultérieures, un délai d’attente lève un TaskCanceledException contenant un TimeoutException. Sur les versions antérieures de .NET Core, l'exception imbriquée n'est pas là. Sur .NET Framework, vous obtenez un HttpRequestException à la place. Pour plus d’informations, consultez HttpClient.Timeout et Effectuer des requêtes HTTP avec la classe HttpClient.

Cela TaskCanceledException signifie également qu’une personne a annulé la demande, par exemple un utilisateur qui a fermé la page. Pour distinguer un délai d’expiration d’une annulation, vérifiez ex.InnerException is TimeoutException ou vérifiez si votre propre jeton est annulé.

Gestionnaire de résilience standard

AddStandardResilienceHandler() enchaîne un limiteur de débit, un délai total d’expiration, une nouvelle tentative, un coupe-circuit et un délai d’attente de tentative. Lorsqu’une tentative prend plus de 10 secondes, le délai d’expiration de la tentative l’annule et la stratégie de réessai tente à nouveau : jusqu’à 3 nouvelles tentatives, avec temporisation exponentielle et jitter, en commençant à 2 secondes. Lorsque la requête entière, y compris les nouvelles tentatives, prend plus de 30 secondes, le délai d’expiration total l’annule et votre code reçoit un TimeoutRejectedException.

TimeoutRejectedException dérive de Exception. Ce n’est ni TimeoutException ni HttpRequestException, donc un bloc catch (HttpRequestException) ne l’intercepte pas. Pour obtenir la liste complète des valeurs par défaut, consultez les valeurs par défaut du gestionnaire de résilience Standard.

Par exemple, lorsqu'une application .NET 10 qui utilise les valeurs par défaut du gestionnaire standard appelle une API qui prend 11 à 15 secondes par réponse, l'application obtient une TimeoutRejectedException après 30 secondes, et son bloc catch (HttpRequestException) ne s'exécute pas.

Guide pratique pour gérer les délais d’expiration HttpClient

  1. Interceptez les deux exceptions lors de l’appel à l’API. Si vous utilisez le gestionnaire standard, interceptez TimeoutRejectedException. Regardez TaskCanceledException pour HttpClient.Timeout.
  2. Distinguez un délai d’expiration d’une annulation. Traitez uniquement un TaskCanceledException comme un délai d’expiration lorsque son exception interne est un TimeoutException. Lorsque l’appelant a annulé, arrêtez en silence.
  3. Sélectionnez les délais d’expiration correspondant à l’API. Si l’API met souvent plus de 10 secondes à répondre, modifiez les délais d’expiration par tentative et total dans AddStandardResilienceHandler(options => ...).
  4. Ne réessayez pas POST ou PATCH, à moins que l’API ne le permette sans risque. Le gestionnaire standard réessaie toutes les méthodes par défaut, y compris POST. Appelez options.Retry.DisableForUnsafeHttpMethods() pour exclure POST, DELETE, PATCH, PUT et CONNECT, ou options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) pour continuer à réessayer les opérations idempotentes PUT et DELETE.
  5. Indiquez à l’utilisateur ce qui s’est passé. Affichez « le service est lent, réessayez » au lieu d’une erreur générique.
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;
    }
}

Comment tester que votre application gère les délais d’expiration

Vous voyez rarement une expiration du délai pendant que vous développez, de sorte que vos blocs catch s’exécutent rarement. La façon dont vous testez détermine si vous trouvez les bogues avant que vos utilisateurs ne le fassent.

Approach Ce que vous trouvez Ce que vous avez manqué
Attendre le passage en production Timeouts en temps réel Tout, jusqu’à ce qu’un utilisateur clique dessus
Simuler l’API dans vos tests ou laisser votre agent de codage écrire la simulation Indique si votre bloc catch s’exécute, si la simulation lève l’exception appropriée Votre configuration réelle HttpClient , les nouvelles tentatives du gestionnaire de résilience et l’exception qu’il lève réellement. Votre application a également besoin d’un commutateur réservé aux tests pour atteindre le mock.
Appeler l’API réelle en espérant qu’elle soit lente Comportement réel Vous ne pouvez pas ralentir l’API intentionnellement
Interceptez le trafic réel de votre application et retarder les réponses Votre vrai HttpClient, gestionnaire de résilience et exceptions Rien dans votre application ne change, donc il ne teste pas votre code de manière isolée. Conservez vos tests unitaires pour cela.

Essayez-le dans votre application

Dev Proxy intercepte les demandes de votre application à l’API et retarde les réponses avec LatencyPlugin. .NET utilise le proxy système. Vous n'avez donc pas besoin de modifier votre code. Cet exemple retarde chaque réponse de 11 à 15 secondes, plus longtemps que le délai d’expiration de 10 secondes du gestionnaire standard. Remplacez https://api.contoso.com par l’URL de l’API que votre application appelle.

Fichier : 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
  }
}

Démarrez le proxy de développement avec devproxy --config-file devproxyrc.json et exécutez votre application. Chaque tentative expire, le gestionnaire retente et, après 30 secondes, votre code reçoit un TimeoutRejectedException. Pour tester HttpClient.Timeout à la place, définissez minMs sur une valeur supérieure au délai d’expiration que vous avez configuré. Pour plus d’informations sur l’installation, consultez Utiliser le proxy de développement avec les applications .NET. Pour installer Dev Proxy, consultez Configurer Dev Proxy.

Étapes suivantes

Voir aussi