.NET HttpClient-timeouter: TaskCanceledException och TimeoutRejectedException

När ett API är för långsamt får din .NET-app 1 av 2 undantag, beroende på vilken timeout som utlöses. HttpClient.Timeout kastar en TaskCanceledException. Standardhanteraren för resiliens från Microsoft.Extensions.Http.Resilience genererar en TimeoutRejectedException från Polly. De kommer från olika platser, har olika standardvärden och behöver separata catch block.

Vilken timeout utlöstes?

Tidsavbrott Standardvärde Vad din kod får Där du ställer in den
HttpClient.Timeout 100 sekunder TaskCanceledException, med en TimeoutException som InnerException (.NET 5 och senare) HttpClient.Timeout
Tidsgräns för standardhanterarförsök 10 sekunder per försök Ingenting först: hanteraren gör ett nytt försök AddStandardResilienceHandler(options => ...)
Total tidsgräns för standardhanterare 30 sekunder, inklusive alla återförsök Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout gäller för varje begäran som HttpClient-instansen skickar. Om du vill använda en annan timeout för en enda begäran skickar du en CancellationToken från en CancellationTokenSource med en egen timeout. Den kortare av de två gäller. Ange Timeout.InfiniteTimeSpan för att stänga av den.

På .NET 5 och senare kastar en timeout en TaskCanceledException med en TimeoutException inuti. I äldre versioner av .NET Core finns inte det inre undantaget. På .NET Framework får du en HttpRequestException i stället. Mer information finns i HttpClient.Timeout och Gör HTTP-begäranden med klassen HttpClient.

En TaskCanceledException innebär också att någon avbröt begäran, till exempel en användare som stängde sidan. Om du vill skilja en timeout från en annullering kontrollerar du ex.InnerException is TimeoutException eller kontrollerar om din egen token har avbrutits.

Standardhanteraren för resiliens

AddStandardResilienceHandler() kedjar en frekvensbegränsare, en total timeout, ett återförsök, en kretsbrytare och en timeout för ett försök. När ett försök tar längre tid än 10 sekunder avbryts det av tidsgränsen för försöket och återförsöksstrategin försöker igen: upp till 3 återförsök, med exponentiell backoff och jitter, från och med 2 sekunder. När hela begäran, inklusive återförsök, tar längre tid än 30 sekunder avbryter den totala tidsgränsen den och koden får en TimeoutRejectedException.

TimeoutRejectedException härleds från Exception. Det är varken en TimeoutException eller ett HttpRequestException, så ett catch (HttpRequestException) block fångar det inte. Den fullständiga listan över standardvärden finns i Standardvärden för standardhanteraren för återhämtning.

När till exempel en .NET 10-app som använder standardhanterarens standardinställningar anropar ett API som tar 11 till 15 sekunder per svar, får appen en TimeoutRejectedException efter 30 sekunder och dess catch (HttpRequestException)-block körs inte.

Hantera HttpClient-timeouter

  1. Fånga båda undantagen där du anropar API:et. Om du använder standardhanteraren, fånga TimeoutRejectedException. Hämta TaskCanceledException för HttpClient.Timeout.
  2. Skilj en timeout från en avbrytning. Behandla endast en TaskCanceledException som en timeout när dess inre undantag är en TimeoutException. När anroparen avbröt, avsluta tyst.
  3. Välj tidsgränser som passar API:et. Om API:et ofta tar längre tid än 10 sekunder ändrar du tidsgränsen för försök och den totala tidsgränsen i AddStandardResilienceHandler(options => ...).
  4. Försök inte igen POST eller PATCH såvida inte API:et gör det säkert. Standardhanteraren försöker som standard alla metoder igen, inklusive POST. Anropa options.Retry.DisableForUnsafeHttpMethods() för att exkludera POST, PATCH, PUT, DELETE och CONNECT, eller options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) för att fortsätta göra nya försök att utföra idempotenta PUT och DELETE.
  5. Berätta för användaren vad som hände. Visa "tjänsten är seg, försök igen" i stället för ett allmänt fel.
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;
    }
}

Så här testar du att din app hanterar timeoutar

Du ser sällan att en tidsgräns överskrids när du utvecklar, så dina catch-block körs nästan aldrig. Hur du testar avgör om du hittar buggarna innan användarna gör det.

Approach Det här hittar du Vad du saknar
Vänta på produktionsmiljön Realtidsgränser Allt, tills en användare trycker på den
Mocka API:et i dina tester eller låt kodningsagenten skriva en mock Huruvida ditt catch-block körs och om mocken genererar rätt undantag Din verkliga HttpClient konfiguration, resilienshanterarens återförsök och undantaget som den faktiskt utlöser. Din app behöver också en testinställning för att nå mocken.
Anropa det verkliga API:et och hoppas att det går långsamt Faktiskt beteende Du kan inte göra API:et långsamt när du vill
Fånga upp appens verkliga trafik och fördröja svaren Din verkliga HttpClient, resilience-hanterare och undantag Ingenting i din app ändras, så den testar inte din kod isolerat. Spara det till enhetstesterna.

Prova det i din app

Dev Proxy fångar upp appens begäranden till API:et och fördröjer svaren med LatencyPlugin. .NET använder systemproxyn, så du behöver inte ändra koden. Det här exemplet fördröjer varje svar med 11 till 15 sekunder, längre än standardhanterarens tidsgräns för försök på 10 sekunder. Ersätt https://api.contoso.com med URL:en för API:et som appen anropar.

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

Starta Dev Proxy med devproxy --config-file devproxyrc.json och kör din app. Varje försök överskrider tidsgränsen, hanteraren försöker igen och efter 30 sekunder får din kod ett TimeoutRejectedException. Testa HttpClient.Timeout i stället genom att ange minMs högre än den tidsgräns som du har konfigurerat. Mer information om konfiguration finns i Använd Dev Proxy med .NET-program. Om du vill installera Dev Proxy, se Konfigurera Dev Proxy.

Nästa steg

Se även