.NET batas waktu HttpClient: TaskCanceledException dan TimeoutRejectedException

Saat API terlalu lambat, aplikasi .NET Anda mendapatkan 1 dari 2 eksepsi, bergantung pada timeout mana yang terpicu. HttpClient.Timeout melempar sebuah TaskCanceledException. Handler ketahanan standar dari Microsoft.Extensions.Http.Resilience melemparkan TimeoutRejectedException dari Polly. Mereka berasal dari tempat yang berbeda, memiliki default yang berbeda, dan membutuhkan blok terpisah catch .

Timeout mana yang terpicu

Jeda Waktu Default Apa yang didapat kode Anda Di tempat Anda menyetelnya
HttpClient.Timeout 100 detik TaskCanceledException, dengan TimeoutException sebagai InnerException (.NET 5 dan yang lebih baru) HttpClient.Timeout
Batas waktu upaya handler standar 10 detik per upaya Awalnya tidak ada respons: handler mencoba kembali upaya AddStandardResilienceHandler(options => ...)
Batas waktu total untuk penangan standar 30 detik, termasuk semua percobaan ulang Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout berlaku untuk setiap permintaan yang dikirim oleh instans HttpClient. Untuk menggunakan batas waktu yang berbeda untuk satu permintaan, teruskan CancellationToken dari CancellationTokenSource dengan batas waktunya sendiri. Yang lebih pendek dari keduanya berlaku. Setel Timeout.InfiniteTimeSpan untuk mematikannya.

Pada .NET 5 dan yang lebih baru, ketika terjadi timeout, TaskCanceledException dilempar dengan TimeoutException di dalamnya. Pada versi .NET Core yang lebih lama, tidak terdapat inner exception. Pada .NET Framework, Anda akan mendapatkan HttpRequestException sebagai gantinya. Untuk detailnya, lihat HttpClient.Timeout dan Membuat permintaan HTTP dengan kelas HttpClient.

TaskCanceledException juga berarti bahwa seseorang membatalkan permintaan, misalnya, pengguna yang menutup halaman. Untuk membedakan timeout dari pembatalan, periksa ex.InnerException is TimeoutException, atau periksa apakah token Anda sendiri telah dibatalkan.

Handler ketahanan standar

AddStandardResilienceHandler() merangkai pembatas laju, batas waktu total, coba lagi, pemutus sirkuit, dan batas waktu upaya. Ketika satu upaya membutuhkan waktu lebih dari 10 detik, batas waktu upaya membatalkannya dan strategi percobaan ulang akan mencoba kembali: hingga 3 percobaan ulang, dengan backoff eksponensial dan jitter, mulai dari 2 detik. Ketika seluruh permintaan, termasuk percobaan ulang, membutuhkan waktu lebih dari 30 detik, total batas waktu membatalkannya dan kode Anda menerima TimeoutRejectedException.

TimeoutRejectedException berasal dari Exception. Ini bukan TimeoutException maupun HttpRequestException, sehingga blok catch (HttpRequestException) tidak menangkapnya. Untuk daftar lengkap default, lihat Default handler ketahanan standar.

Misalnya, ketika aplikasi .NET 10 yang menggunakan default handler standar memanggil API yang membutuhkan waktu 11 hingga 15 detik per respons, aplikasi mendapatkan TimeoutRejectedException setelah 30 detik, dan blok catch (HttpRequestException)-nya tidak berjalan.

Cara menangani batas waktu HttpClient

  1. Tangkap kedua pengecualian saat Anda memanggil API. Jika Anda menggunakan handler standar, tangkap TimeoutRejectedException. Dapatkan TaskCanceledException untuk HttpClient.Timeout.
  2. Bedakan timeout dari pembatalan. Hanya memperlakukan TaskCanceledException sebagai batas waktu ketika pengecualian dalamnya adalah TimeoutException. Ketika penelepon membatalkan, berhenti diam-diam.
  3. Pilih batas waktu yang sesuai dengan API. Jika API sering memakan waktu lebih dari 10 detik, ubah batas waktu percobaan dan total di AddStandardResilienceHandler(options => ...).
  4. Jangan ulangi POST atau PATCH kecuali API membuatnya aman. Handler standar mencoba kembali semua metode secara default, termasuk POST. Panggil options.Retry.DisableForUnsafeHttpMethods() untuk mengecualikan POST, PUT, PATCH, DELETE, dan CONNECT, atau options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) untuk terus mencoba kembali PUT dan DELETE yang idempoten.
  5. Beri tahu pengguna apa yang terjadi. Tampilkan "layanan sedang lambat, coba lagi" alih-alih kesalahan umum.
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;
    }
}

Cara menguji bahwa aplikasi Anda menangani batas waktu

Anda jarang melihat timeout saat mengembangkan, sehingga blok Anda catch jarang berjalan. Cara Anda menguji memutuskan apakah Anda menemukan bug sebelum pengguna Anda melakukannya.

Approach Apa yang Anda temukan Yang Anda lewatkan
Tunggu hingga siap produksi Timeout sebenarnya Semuanya, sampai pengguna mengkliknya
Mock API dalam pengujian Anda, atau biarkan agen coding Anda menulis mock Apakah blok Anda catch berjalan jika tiruan melempar pengecualian yang tepat? Konfigurasi sebenarnya HttpClient Anda, percobaan ulang handler ketahanan, dan pengecualian yang sebenarnya dilemparkannya. Aplikasi Anda juga memerlukan sakelar khusus pengujian untuk mengakses mock.
Panggil API nyata dan berharap API tersebut lambat Perilaku nyata Anda tidak dapat membuat API lambat secara sengaja
Menyadap lalu lintas nyata aplikasi Anda dan menunda respons Penangan nyata Anda, penangan ketahanan, dan pengecualian HttpClient Tidak ada yang berubah pada aplikasi Anda, sehingga tidak menguji kode Anda secara terisolasi. Gunakan pengujian unit untuk itu.

Coba ini di aplikasi Anda

Dev Proxy mencegat permintaan aplikasi Anda ke API dan menunda respons dengan LatencyPlugin. .NET menggunakan proksi sistem, jadi Anda tidak perlu mengubah kode Anda. Contoh ini menunda setiap respons hingga 11 sampai 15 detik, lebih lama dari batas waktu upaya 10 detik milik handler standar. Ganti https://api.contoso.com dengan URL API yang dipanggil aplikasi Anda.

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

Mulai Dev Proxy dengan devproxy --config-file devproxyrc.json dan jalankan aplikasi Anda. Setiap kali upaya mengalami time-out, handler mencoba kembali, dan setelah 30 detik kode Anda mengembalikan TimeoutRejectedException. Untuk menguji HttpClient.Timeout , atur minMs lebih tinggi dari batas waktu yang Anda konfigurasikan. Untuk detail penyiapan, lihat Menggunakan Dev Proxy dengan aplikasi .NET. Untuk menginstal Dev Proxy, lihat Menyiapkan Dev Proxy.

Langkah berikutnya

Baca juga