.NET timeout HttpClient: TaskCanceledException e TimeoutRejectedException

Quando un'API è troppo lenta, l'app .NET ottiene 1 di 2 eccezioni, a seconda di quale timeout è scattato. HttpClient.Timeout genera un'eccezione TaskCanceledException. Il gestore della resilienza standard di Microsoft.Extensions.Http.Resilience genera un oggetto TimeoutRejectedException da Polly. Provengono da luoghi diversi, hanno impostazioni predefinite diverse e necessitano di blocchi separati catch.

Quale timeout si è attivato?

Interruzione temporanea Impostazione predefinita Cosa ottiene il tuo codice Dove l'hai impostato
HttpClient.Timeout 100 secondi TaskCanceledException, con TimeoutException come InnerException (.NET 5 e versioni successive) HttpClient.Timeout
Timeout del tentativo del gestore standard 10 secondi per tentativo All'inizio non accade nulla: il gestore ripete il tentativo AddStandardResilienceHandler(options => ...)
Timeout totale del gestore standard 30 secondi, inclusi tutti i tentativi Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout si applica a ogni richiesta inviata dall'istanza HttpClient . Per usare un timeout diverso per una richiesta, passare un CancellationToken da un CancellationTokenSource con il proprio timeout. Si applica il più breve dei due. Impostare Timeout.InfiniteTimeSpan per disattivarlo.

In .NET 5 e versioni successive, un timeout genera un TaskCanceledException con un TimeoutException all'interno. Nelle versioni precedenti di .NET Core l'eccezione interna non è presente. In .NET Framework si ottiene invece un oggetto HttpRequestException . Per informazioni dettagliate, vedere HttpClient.Timeout e Effettuare richieste HTTP con la classe HttpClient.

Un TaskCanceledException significa anche che un utente ha annullato la richiesta, ad esempio un utente che ha chiuso la pagina. Per distinguere un timeout da un annullamento, controllare ex.InnerException is TimeoutException oppure verificare se il proprio token è annullato.

Gestore della resilienza standard

AddStandardResilienceHandler() concatena un limitatore di velocità, un timeout totale, un nuovo tentativo, un circuit breaker e un timeout di tentativo. Quando un tentativo richiede più di 10 secondi, il timeout del tentativo lo annulla e la strategia di ripetizione dei tentativi tenta di nuovo: fino a 3 tentativi, con backoff esponenziale e jitter, a partire da 2 secondi. Quando l'intera richiesta, inclusi i tentativi, richiede più di 30 secondi, il timeout totale la annulla e il codice ottiene un TimeoutRejectedException.

TimeoutRejectedException deriva da Exception. Non è né un oggetto TimeoutException né un HttpRequestExceptionoggetto , quindi un catch (HttpRequestException) blocco non lo intercetta. Per l'elenco completo delle impostazioni predefinite, vedere Valori predefiniti del gestore di resilienza standard.

Ad esempio, quando un'app .NET 10 che usa le impostazioni predefinite del gestore standard chiama un'API che richiede da 11 a 15 secondi per risposta, l'app ottiene un valore TimeoutRejectedException dopo 30 secondi e il relativo catch (HttpRequestException) blocco non viene eseguito.

Come gestire i timeout di HttpClient

  1. Intercettare entrambe le eccezioni in cui si chiama l'API. Se si usa il gestore standard, intercettare TimeoutRejectedException. Catch TaskCanceledException per HttpClient.Timeout.
  2. Distinguere un timeout da un annullamento. Considerare un oggetto TaskCanceledException come timeout solo quando l'eccezione interna è un oggetto TimeoutException. Quando il chiamante ha annullato, fermarsi in modo silenzioso.
  3. Scegli timeout adatti all'API. Se l'API richiede spesso più di 10 secondi, modificare il tentativo e i timeout totali in AddStandardResilienceHandler(options => ...).
  4. Non riprovare POST o PATCH a meno che l'API non garantisca che sia sicuro farlo. Il gestore standard ritenta tutti i metodi per impostazione predefinita, incluso POST. Chiamare options.Retry.DisableForUnsafeHttpMethods() per escludere POST, PATCH, PUT, DELETE, e CONNECT, o options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) per continuare a ripetere idempotenti PUT e DELETE.
  5. Indicare all'utente cosa è successo. Mostra "il servizio è lento, riprovare" anziché un errore generico.
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;
    }
}

Come testare che l'app gestisca i timeout

Raramente viene visualizzato un timeout durante lo sviluppo, quindi i catch blocchi vengono eseguiti raramente. Il modo in cui si testa decide se trovi i bug prima che li trovino gli utenti.

Avvicinarsi Cosa trovi Quello che ti manca
Attendere l'ambiente di produzione Timeout in tempo reale Tutto, fino a quando un utente non ci clicca
Simula l'API nei test oppure lascia che il tuo agente di coding scriva il mock Se il catch blocco viene eseguito, se la simulazione genera l'eccezione corretta La configurazione reale HttpClient, i tentativi del gestore di resilienza e l'eccezione che viene effettivamente generata. L'app richiede anche un'opzione di sola prova per raggiungere il mock.
Chiamare l'API reale e sperare che sia lenta Comportamento reale Non puoi rendere lenta l'API su richiesta
Intercettare il traffico reale dell'app e ritardare le risposte Il gestore reale HttpClient, il gestore della resilienza e le eccezioni Niente nella tua app cambia, quindi non testa il codice in isolamento. Mantieni i test unitari per quello.

Provalo nella tua app

Dev Proxy intercetta le richieste dell'app all'API e ritarda le risposte con LatencyPlugin. .NET usa il proxy di sistema, quindi non è necessario modificare il codice. Questo esempio ritarda ogni risposta da 11 a 15 secondi, più a lungo del timeout di tentativo di 10 secondi del gestore standard. Sostituire https://api.contoso.com con l'URL dell'API chiamata dall'app.

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

Avviare Dev Proxy con devproxy --config-file devproxyrc.json ed eseguire l'app. Ogni tentativo raggiunge il timeout, il gestore ritenta e dopo 30 secondi il codice riceve TimeoutRejectedException. Per eseguire il test HttpClient.Timeout , impostare minMs invece un valore superiore al timeout configurato. Per informazioni dettagliate sull'installazione, vedere Usare Dev Proxy con applicazioni .NET. Per installare Dev Proxy, vedere Configurare Dev Proxy.

Passaggi successivi

Vedere anche