.NET vypršení časového limitu HttpClient: TaskCanceledException a TimeoutRejectedException

Pokud je rozhraní API příliš pomalé, aplikace .NET získá 1 ze 2 výjimek v závislosti na tom, který časový limit se aktivoval. HttpClient.Timeout vyvolá TaskCanceledException. Standardní obslužná rutina odolnosti z Microsoft.Extensions.Http.Resilience vyvolá TimeoutRejectedException z Polly. Pocházejí z různých míst, mají různé výchozí hodnoty a potřebují samostatné catch bloky.

Který časový limit vypršel

Přerušení zápasu Výchozí Co váš kód obdrží Kde ho nastavíte
HttpClient.Timeout 100 sekund TaskCanceledException, s TimeoutException jako InnerException (.NET 5 a novější) HttpClient.Timeout
Standardní časový limit pro pokus obslužné rutiny 10 sekund za pokus Zpočátku nic: obslužná rutina pokus opakuje. AddStandardResilienceHandler(options => ...)
Celkový časový limit standardního handleru 30 sekund včetně všech opakování Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout platí pro každý požadavek, který odešle instance HttpClient. Pokud chcete pro jeden požadavek použít jiný časový limit, předejte CancellationToken z CancellationTokenSource s vlastním časovým limitem. Kratší z těchto dvou možností platí. Nastavte Timeout.InfiniteTimeSpan na vypnuto.

V .NET 5 a novějších časový limit vyvolá TaskCanceledException s vnitřní výjimkou TimeoutException. V dřívějších verzích .NET Core vnitřní výjimka není k dispozici. V rozhraní .NET Framework místo toho získáte HttpRequestException. Podrobnosti najdete v tématu HttpClient.Timeout a provádění požadavků HTTP pomocí třídy HttpClient.

A TaskCanceledException také znamená, že někdo žádost zrušil, například uživatel, který stránku zavřel. Pokud chcete rozlišit časový limit od zrušení, zkontrolujte ex.InnerException is TimeoutException nebo zkontrolujte, zda byl váš vlastní token zrušen.

Standardní zpracování odolnosti

AddStandardResilienceHandler() zřetězuje omezovač rychlosti požadavků, celkový časový limit, opakování, circuit breaker a časový limit pokusu. Pokud jeden pokus trvá déle než 10 sekund, časový limit pokusu ho zruší a strategie opakování se pokusí znovu: až 3 opakování s exponenciálním zpoždováním a náhodným rozptylem, počínaje 2 sekundami. Pokud celý požadavek, včetně opakování, trvá déle než 30 sekund, celkový časový limit ho zruší a vašemu kódu se vrátí TimeoutRejectedException.

TimeoutRejectedException je odvozeno z Exception. Není to ani TimeoutException, ani HttpRequestException, takže ho blok catch (HttpRequestException) nezachytí. Úplný seznam výchozích hodnot najdete v tématu Standardní výchozí hodnoty obslužné rutiny pro zajištění odolnosti.

Když například aplikace .NET 10, která používá výchozí nastavení standardní obslužné rutiny, volá rozhraní API, které trvá 11 až 15 sekund na odpověď, aplikace po 30 sekundách obdrží TimeoutRejectedException a její catch (HttpRequestException) blok se nespustí.

Jak řešit časové limity HttpClient

  1. Zachyťte obě výjimky tam, kde voláte rozhraní API. Pokud používáte standardní obslužnou proceduru, použijte TimeoutRejectedException. Zachytit TaskCanceledException pro HttpClient.Timeout.
  2. Rozlišit vypršení časového limitu od zrušení Za časový limit považujte TaskCanceledException pouze tehdy, když je jeho vnitřní výjimkou TimeoutException. Když volající zruší, tiše zastavte.
  3. Vyberte časové limity, které odpovídají rozhraní API. Pokud odpověď API často trvá déle než 10 sekund, změňte časový limit pokusu a celkový časový limit v AddStandardResilienceHandler(options => ...).
  4. Neopakujte POST ani PATCH, pokud to rozhraní API neoznačí jako bezpečné. Standardní obslužná rutina ve výchozím nastavení opakuje všechny metody, včetně POST. Zavolejte options.Retry.DisableForUnsafeHttpMethods(), chcete-li vyloučit POST, PATCH, DELETE, PUT a CONNECT, nebo zavolejte options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch), chcete-li pokračovat v opakování idempotentních PUT a DELETE.
  5. Řekněte uživateli, co se stalo. Místo obecné chyby zobrazte "služba je pomalá, zkuste to znovu".
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;
    }
}

Jak otestovat, že aplikace zvládá timeouty

Při vývoji se vypršení časového limitu zřídka objeví, takže se vaše bloky catch zřídka spouštějí. Způsob, jakým testujete, rozhoduje o tom, jestli chyby zjistíte dříve, než je najdou vaši uživatelé.

Approach Co najdete Co vám chybí
Počkejte na produkci Skutečné časové limity Všechno, dokud na něj uživatel neklikne
V testech mockujte API nebo nechte agenta pro psaní kódu napsat mock Zda se váš blok catch spustí, když mock vyvolá správnou výjimku Vaše skutečné HttpClient nastavení, opakované pokusy handleru odolnosti a výjimka, kterou skutečně vyvolá. Vaše aplikace také potřebuje testovací přepínač pro přístup k mocku.
Zavolejte skutečné rozhraní API a doufejte, že je pomalé Skutečné chování Rozhraní API nejde na vyžádání zpomalit.
Zachyťte skutečný provoz aplikace a zpozděte odpovědi Vaše skutečná HttpClientobslužná rutina pro zajištění odolnosti a výjimky V aplikaci se nic nemění, takže netestuje váš kód izolovaně. Na to si nechte jednotkové testy.

Vyzkoušejte si ji v aplikaci

Dev Proxy zachytává požadavky vaší aplikace na rozhraní API a zpožďuje odpovědi s LatencyPlugin. .NET používá systémový proxy server, takže kód nemusíte měnit. Tento příklad zpožďuje každou odpověď o 11 až 15 sekund, což je déle než standardní 10sekundový časový limit pokusu. Nahraďte https://api.contoso.com adresou URL rozhraní API, které vaše aplikace volá.

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

Spusťte Dev Proxy pomocí devproxy --config-file devproxyrc.json a spusťte aplikaci. U každého pokusu dojde k vypršení časového limitu, handler to zkusí znovu a po 30 sekundách je vašemu kódu vráceno TimeoutRejectedException. Pokud chcete místo toho testovat HttpClient.Timeout , nastavte minMs vyšší než časový limit, který jste nakonfigurovali. Podrobnosti o nastavení najdete v tématu Použití Dev Proxy s aplikacemi .NET. Pokud chcete nainstalovat Dev Proxy, přečtěte si téma Nastavení Dev Proxy.

Další kroky

Viz také