正確的使用 HttpClient (遠端 SSL 憑證無效問題)

為工作紀錄。解決遠端 SSL 憑證無效的錯誤問題。IHttpClientFactory, AddHttpClient

引言

.NET Core 的 HttpClient的資源釋放做的不完美,在 Dispose 後還會有殘留,官方解法是另設計了一個 IHttpClientFactory來統一管理。

總之,就是不要直接用 HttpClient 建構,改透過 IHttpClientFactory 間接建構 httpClient 物件。

HttpClient 的連線集區會連結至基礎 SocketsHttpHandler。 當您處置 HttpClient 執行個體時,其會處置集區內的所有現有連線。 如果您稍後將要求傳送至相同的伺服器,則必須重新建立新的連線。 因此,不必要的連線建立會產生效能損失。 此外,連線關閉之後,不會立即釋放 TCP 連接埠。 (如需詳細資訊,請參閱 RFC 9293 中的 TCP TIME-WAIT。)如果要求率很高,則可能會耗盡可用連接埠的作業系統限制。 若要避免連接埠耗盡問題,建議您盡可能重複使用多個 HTTP 要求的 HttpClient 執行個體。

參考文件

官方 HtppClient 標準答案。

環境

IDE:Visual Studio 2022

平台:.NET6

相依套件

程式碼紀錄:法一

為每個存取目標網站,撰寫相應的存取服務。

Program.cs
using System;

var builder = WebApplication.CreateBuilder(args);
...略...

// 註冊 HttpClient 工廠使可注入:IHttpClientFactory 以建構較低秏的 HttpClient 資源池。
builder.Services.AddHttpClient("FooWebApiUrl", c =>
{
  c.BaseAddress = new Uri(Configuration["FooWebApiUrl"]);
  // headers...
  //c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
  //c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
  //## 解決遠端 SSL 憑證無效的錯誤問題
  //※ 注意:跳過憑證檢查,沒問題不要加這段碼。
  var handler = new System.Net.Http.HttpClientHandler();
  handler.ClientCertificateOptions = System.Net.Http.ClientCertificateOption.Manual;
  handler.ServerCertificateCustomValidationCallback = (httpRequestMessage, cert, cetChain, policyErrors) => true;
  return handler;
});

...略...
var app = builder.Build();
...略...
app.Run();
using System.Net.Http;

class YourService : IDisposable
{
  //# Injection Member
  readonly HttpClient _http;

  void IDisposable.Dispose()
  {
    _http?.Dispose();
  }
  
  public YourService(IHttpClientFactory httpFactory)
  {
    //※ 其中 IHttpClientFactory 介面來自 NuGet 套件:Microsoft.Extensions.Http。
    _http= httpFactory.CreateClient("FooWebApiUrl");
  }

  void CallFooWebApi(string id)
  {
    ...略...
    using var ret = _http.PostAsync($"api/foo/{id}", null);    
    ...略...
  }
}

程式碼紀錄:法二

Program.cs
var builder = WebApplication.CreateBuilder(args);
...略...

// 註冊 HttpClient 工廠使可注入:IHttpClientFactory 以建構較低秏的 HttpClient 資源池。
builder.Services.AddHttpClient();

/// 註冊:以 HttpClient 為基底的客製服務
builder.Services.AddScoped<FooApiService>(); 
 
...略...
var app = builder.Build();
...略...
app.Run();

注入 IHttpClientFactory 建構 HttpClient 等相關資料實作高階的、商業邏輯等級的通訊指令。

FooApiService.cs
using System.Net.Http;
using System.Net.Http.Json;

internal class FooApiService
{
  //# Injection Member
  readonly IHttpClientFactory _httpFactory;
  
  //# State
  public YourAccessToken AccessToken { get; private set; } = null;

  public FooApiService(IHttpClientFactory httpFactory)
  {
    _httpFactory = httpFactory;
  }

  public async Task<FooBarResp> CallFooBarApiAsync()
  {
    try
    {
      if(this.AccessToken == null)
        await GetTokenAsync();

      using var _http = _httpFactory.CreateClient();
      _http.DefaultRequestHeaders.Add("authorization", $"Bearer {this.AccessToken.bearer_token}");
      _http.BaseAddress = new Uri($"https://target.webapi.url");

      using var resp = await _http.GetAsync(@$"api/FooBar?$format=JSON");

      var json = await resp.Content.ReadAsStringAsync();
      var result = JsonSerializer.Deserialize<FooBarResp>(json);
      return result;
    }
    catch(Exception ex)
    {
      throw new ApplicationException("出現例外!" + ex.Message, ex);
    }
  }

  private async Task GetTokenAsync()
  {
    try
    {
      HttpContent formData = new FormUrlEncodedContent(
          new List<KeyValuePair<string, string>>
              {
                  new KeyValuePair<string, string>("grant_type", "client_credentials"),
                  new KeyValuePair<string, string>("client_id", "xxxxxx"),
                  new KeyValuePair<string, string>("client_secret", "xxxxxx"),
              }
          );

      using var _http = _httpFactory.CreateClient();
      _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
      _http.BaseAddress = new Uri($"https://target.webapi.url");

      using var resp = await _http.PostAsync("auth/token", formData);
      var token = await resp.Content.ReadFromJsonAsync<YourAccessToken>();

      //# 存放 access token
      this.AccessToken = token;
    }
    catch ( Exception ex )
    {
      throw new HttpRequestException("授權要求失敗!", ex, HttpStatusCode.Unauthorized);
    }
  }
}

Last updated