Masalah penggunaan umum IHttpClientFactory

Dalam artikel ini, Anda akan mempelajari beberapa masalah paling umum yang dapat Anda temui saat menggunakan IHttpClientFactory untuk membuat HttpClient instans.

IHttpClientFactory adalah cara mudah untuk menyiapkan beberapa HttpClient konfigurasi dalam kontainer DI, mengonfigurasi pengelogan, menyiapkan strategi ketahanan, dan banyak lagi. IHttpClientFactory juga merangkum pengelolaan masa pakai instans HttpClient dan HttpMessageHandler, untuk mencegah masalah seperti kelelahan soket dan kehilangan perubahan DNS. Untuk gambaran umum tentang cara menggunakan IHttpClientFactory di aplikasi .NET Anda, lihat IHttpClientFactory dengan .NET.

Karena sifat kompleks integrasi IHttpClientFactory dengan DI, Anda dapat mengalami beberapa masalah yang mungkin sulit dideteksi dan diatasi. Skenario yang tercantum dalam artikel ini juga berisi rekomendasi, yang dapat Anda terapkan secara proaktif untuk menghindari potensi masalah.

HttpClient tidak menghormati Scoped masa pakai

Anda dapat mengalami masalah jika Anda perlu mengakses layanan dengan ruang lingkup terbatas apa pun, misalnya, HttpContext, atau beberapa cache dengan ruang lingkup terbatas, dari dalam HttpMessageHandler. Data yang disimpan di sana dapat "menghilang", atau, sebaliknya, "bertahan" ketika seharusnya tidak. Ini disebabkan oleh ketidakcocokan cakupan Dependency Injection (DI) antara konteks aplikasi dan instans handler, dan ini merupakan batasan yang sudah diketahui dalam IHttpClientFactory.

IHttpClientFactory membuat cakupan DI terpisah untuk setiap HttpMessageHandler instance. Cakupan handler ini berbeda dari cakupan konteks aplikasi (misalnya, ASP.NET cakupan permintaan masuk Core, atau cakupan DI manual yang dibuat pengguna), sehingga mereka tidak akan berbagi instans layanan yang tercakup.

Sebagai hasil dari batasan ini:

  • Data apa pun yang di-cache "secara eksternal" dalam layanan terlingkup tidak akan tersedia dalam HttpMessageHandler.
  • Setiap data yang di-cache "secara internal" dalam HttpMessageHandler atau dependensi cakupannya dapat diamati dari beberapa cakupan DI aplikasi (misalnya, dari permintaan masuk yang berbeda) karena dapat berbagi handler yang sama.

Pertimbangkan rekomendasi berikut untuk membantu meringankan batasan yang diketahui ini:

❌ JANGAN menyimpan informasi terkait cakupan apa pun (seperti data dari HttpContext) di dalam HttpMessageHandler instans atau dependensinya untuk menghindari kebocoran informasi sensitif.

❌ JANGAN gunakan cookie, karena CookieContainer akan dibagikan bersama dengan handler.

✔️ PERTIMBANGKAN untuk tidak menyimpan informasi, atau hanya meneruskannya dalam instans HttpRequestMessage.

Untuk meneruskan informasi arbitrer bersama HttpRequestMessage, Anda dapat menggunakan properti HttpRequestMessage.Options.

✔️ Pertimbangkan untuk mengenkapsulasi semua logika yang terkait dengan cakupan (misalnya, autentikasi) dalam DelegatingHandler yang terpisah dan tidak dibuat oleh IHttpClientFactory, dan gunakan untuk membungkus pengendali yang dibuat oleh IHttpClientFactory.

Untuk membuat hanya sebuah HttpMessageHandler tanpa HttpClient, panggil IHttpMessageHandlerFactory.CreateHandler untuk setiap klien bernama yang terdaftar. Dalam hal ini, Anda harus membuat HttpClient instansi sendiri menggunakan handler gabungan. Anda dapat menemukan contoh yang dapat dijalankan sepenuhnya untuk solusi ini di GitHub.

Untuk informasi selengkapnya, lihat bagian Cakupan Penanganan Pesan di IHttpClientFactory dalam IHttpClientFactory panduan.

HttpClient tidak menghormati perubahan DNS

Bahkan jika IHttpClientFactory digunakan, masih mungkin untuk mengalami masalah DNS basi. Ini biasanya dapat terjadi jika HttpClient instans ditangkap dalam Singleton layanan, atau, secara umum, disimpan di suatu tempat untuk jangka waktu yang lebih lama dari yang ditentukan HandlerLifetime. HttpClientjuga akan ditangkap jika klien jenis masing-masing ditangkap oleh singleton.

❌ JANGAN menyimpan HttpClient instans yang dibuat oleh IHttpClientFactory untuk waktu yang lama.

❌ JANGAN menyuntikkan instans klien yang ditik ke dalam Singleton layanan.

✔️ PERTIMBANGKAN untuk meminta klien dari IHttpClientFactory secara tepat waktu atau setiap kali Anda membutuhkannya. Klien perangkat lunak yang diciptakan secara otomatis aman untuk dibuang.

HttpClientinstans yang dibuat oleh IHttpClientFactory dimaksudkan untuk berumur pendek.

  • Mendaur ulang dan membuat HttpMessageHandlerulang ketika masa pakainya kedaluwarsa sangat penting untuk IHttpClientFactory memastikan handler bereaksi terhadap perubahan DNS. HttpClient terkait dengan instans handler tertentu setelah pembuatannya, sehingga instans baru HttpClient harus diminta tepat waktu untuk memastikan klien akan mendapatkan handler yang diperbarui.

  • Membuang instans HttpClient yang diciptakan oleh factory tidak akan menyebabkan kehabisan soket, karena pembuangannya tidak memicu pembuangan dari HttpMessageHandler. IHttpClientFactory melacak dan membuang sumber daya yang digunakan untuk membuat HttpClient instans, khususnya HttpMessageHandler instans, segera setelah masa pakainya kedaluwarsa dan tidak ada HttpClient yang menggunakannya lagi.

Klien bertipe juga dimaksudkan untuk berusia pendek karena sebuah instance HttpClient disuntikkan ke dalam konstruktor, sehingga akan berbagi masa pakai klien bertipe.

Untuk informasi selengkapnya, lihat bagian HttpClient manajemen masa pakai dan Hindari klien bertipe di layanan singleton dalam IHttpClientFactory panduan.

HttpClient menggunakan terlalu banyak soket

Bahkan jika IHttpClientFactory digunakan, masih mungkin untuk mengalami masalah kelelahan soket dengan skenario penggunaan tertentu. Secara default, HttpClient tidak membatasi jumlah permintaan bersamaan. Jika sejumlah besar permintaan HTTP/1.1 dimulai secara bersamaan pada saat yang sama, masing-masing permintaan akan akhirnya memicu upaya koneksi HTTP baru, karena tidak ada koneksi gratis di kumpulan dan tidak ada batas yang ditetapkan.

❌ JANGAN memulai sejumlah besar permintaan HTTP/1.1 secara bersamaan pada saat yang sama tanpa menentukan batas.

✔️ PERTIMBANGKAN pengaturan HttpClientHandler.MaxConnectionsPerServer (atau SocketsHttpHandler.MaxConnectionsPerServer, jika Anda menggunakannya sebagai handler utama) ke nilai yang wajar. Perhatikan bahwa batas ini hanya berlaku untuk instans handler tertentu.

✔️ PERTIMBANGKAN untuk menggunakan HTTP/2, yang memungkinkan permintaan multipleks melalui satu koneksi TCP.

Klien bertipe memiliki HttpClient yang salah disuntikkan

Mungkin ada berbagai situasi di mana dimungkinkan secara tidak terduga untuk menyuntikkan HttpClient ke dalam klien yang diketik. Seringkali, akar penyebabnya terletak pada konfigurasi yang salah, karena berdasarkan desain DI, setiap pendaftaran layanan berikutnya menggantikan yang sebelumnya.

Klien berjenis menggunakan klien bernama secara internal: menambahkan klien berjenis secara implisit mendaftarkan dan menautkannya ke klien bernama. Nama klien, kecuali disediakan secara eksplisit, akan diatur ke nama jenis TClient. Ini akan menjadi yang pertama dari TClient,TImplementation pasangan jika AddHttpClient<TClient,TImplementation> kelebihan beban digunakan.

Oleh karena itu, mendaftarkan klien bertipe melakukan dua hal yang berbeda:

  1. Mendaftarkan klien bernama (dalam kasus default sederhana, namanya adalah typeof(TClient).Name).
  2. Transient Mendaftarkan layanan menggunakan TClient atau TClient,TImplementation disediakan.

Dua pernyataan berikut secara teknis sama:

services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));

// -OR-

services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
    .AddTypedClient<ExampleClient>(); // link the named client to a typed client

Dalam kasus sederhana, ini juga akan mirip dengan yang berikut:

services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client

// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
    new ExampleClient(
        s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));

Pertimbangkan contoh berikut tentang bagaimana tautan antara klien yang diketik dan diberi nama bisa rusak.

Typed client didaftarkan untuk kedua kalinya

❌JANGAN mendaftarkan klien yang di ketik secara terpisah — klien sudah terdaftar secara otomatis melalui AddHttpClient<T> panggilan.

Jika klien yang ditik secara keliru mendaftarkan untuk kedua kalinya sebagai layanan Sementara biasa, ini akan menimpa pendaftaran yang ditambahkan oleh IHttpClientFactory, melanggar tautan ke klien bernama. Ini akan terlihat seolah-olah konfigurasi dari HttpClient hilang, karena HttpClient yang belum dikonfigurasi akan dimasukkan ke dalam klien bertipe sebagai gantinya.

Mungkin membingungkan bahwa, alih-alih melempar pengecualian, digunakan "salah" HttpClient. Ini terjadi karena "default" belum dikonfigurasi HttpClient — klien dengan Options.DefaultName nama (string.Empty) — terdaftar sebagai layanan Sementara biasa, untuk mengaktifkan skenario penggunaan yang paling dasar IHttpClientFactory. Itu sebabnya setelah tautan rusak dan klien yang di ketik hanya menjadi layanan biasa, "default" HttpClient ini secara alami akan disuntikkan ke dalam parameter konstruktor masing-masing.

Klien jenis yang berbeda terdaftar pada antarmuka umum

Jika dua klien jenis yang berbeda terdaftar pada antarmuka umum, mereka berdua akan menggunakan kembali klien bernama yang sama. Ini bisa tampak seperti klien yang pertama kali kita ketik mendapatkan klien kedua yang dinamai disuntikkan secara salah.

❌ JANGAN mendaftarkan beberapa klien yang dititik pada satu antarmuka tanpa secara eksplisit menentukan nama.

✔️ PERTIMBANGKAN untuk mendaftarkan dan mengonfigurasi klien bernama secara terpisah, lalu menautkannya ke satu atau beberapa klien bertipe, baik dengan menentukan nama dalam panggilan AddHttpClient<T> atau dengan memanggil AddTypedClient selama penyiapan klien bernama.

Secara desain, mendaftarkan dan mengonfigurasi klien bernama tertentu dengan nama yang sama beberapa kali hanya menambahkan tindakan konfigurasi ke dalam daftar yang sudah ada. Perilaku IHttpClientFactory ini mungkin tidak jelas, tetapi merupakan pendekatan yang sama yang digunakan oleh pola Opsi dan API konfigurasi seperti Configure.

Ini sebagian besar berguna untuk konfigurasi handler tingkat lanjut, misalnya, menambahkan handler kustom ke klien yang bernama ditentukan secara eksternal, atau meniru handler utama untuk pengujian, namun juga berfungsi untuk HttpClient konfigurasi instans. Misalnya, tiga contoh berikut akan menghasilkan HttpClient yang dikonfigurasi dengan cara sama (baik BaseAddress maupun DefaultRequestHeaders diatur).

// one configuration callback
services.AddHttpClient("example", c =>
    {
        c.BaseAddress = new Uri("http://example.com");
        c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
    });

// -OR-

// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
    .ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));

// -OR-

// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
    .ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));

Ini memungkinkan penautan klien berjenis ke klien bernama yang sudah ditentukan, dan juga menautkan beberapa klien berjenis ke satu klien bernama. Lebih jelas ketika kelebihan beban dengan name parameter digunakan:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));

services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");

Hal yang sama juga dapat dicapai dengan memanggil AddTypedClient selama konfigurasi klien bernama:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
    .AddTypedClient<FooLogger>()
    .AddTypedClient<BarLogger>();

Namun, jika Anda tidak ingin menggunakan kembali klien bernama yang sama, tetapi Anda masih ingin mendaftarkan klien pada antarmuka yang sama, Anda dapat melakukannya dengan secara eksplisit menentukan nama yang berbeda untuk mereka:

services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
    c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
    c => c.BaseAddress = new Uri("https://github.com"));

Lihat juga