The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何攔截並記錄 HttpClient 發出的所有 HTTP Request 與 Response 資料

在 .NET 要呼叫 REST API 時,最常使用的就屬 HttpClient 類別莫屬,但有時候真的想看他到底送出了什麼 HTTP 封包,或是收到了什麼 HTTP 回應 (原始內容),這時候就需要一個方法來攔截並記錄實際 HTTP 傳送的 Request 與 Response 封包內容。其實是有方法的,這篇文章就來介紹如何實作。

image

使用 DelegatingHandler 類別

在 .NET 中,我們可以使用 DelegatingHandler 類別來攔截 HttpClient 發出的所有 HTTP Request 與 Response 資料,這個類別是一個抽象類別,我們可以繼承這個類別並實作 SendAsync 方法,這個方法會在每次 HttpClient 發出 Request 時被呼叫,因此我們可以在這個方法中攔截 Request 與 Response 的內容。

以下是一個完整的實作範例:

public class DebugHttpMessageHandler : DelegatingHandler
{
  public DebugHttpMessageHandler() : base(new HttpClientHandler())
  {
  }

  public DebugHttpMessageHandler(HttpMessageHandler handler) : base(handler)
  {
  }

  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    // Log request method and URL
    Console.WriteLine("Request: {0} {1}", request.Method, request.RequestUri);

    // Log headers
    foreach (var header in request.Headers)
    {
      Console.WriteLine("Default Headers: {0}: {1}", header.Key, string.Join(", ", header.Value));
    }

    foreach (var header in request.Content.Headers)
    {
      Console.WriteLine("Content Headers: {0}: {1}", header.Key, string.Join(", ", header.Value));
    }

    // Show request payload
    if (request.Content != null)
    {
      string payload = await request.Content.ReadAsStringAsync();
      Console.WriteLine("Payload: {0}", payload);
    }

    Console.WriteLine();

    // Send the request and get the response
    HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

    // Log response status
    Console.WriteLine("Response: {0} ({1})", response.StatusCode, ((int)response.StatusCode));

    // Log headers
    foreach (var header in response.Headers)
    {
      Console.WriteLine("Headers: {0}: {1}", header.Key, string.Join(", ", header.Value));
    }

    string jsonText = await response.Content.ReadAsStringAsync();
    Console.WriteLine("Response Body:\n{0}", jsonText);

    return response;
  }
}

使用範例:以 Console 應用程式為例

要使用上述 DebugHttpMessageHandler 類別,只要在建立 HttpClient 時指定 DebugHttpMessageHandler 類別即可,但要記得傳入一個 HttpClientHandler 實例,以下是一個使用範例:

// v1
// HttpClientHandler handler = new HttpClientHandler {
//   UseDefaultCredentials = true,
//   PreAuthenticate = true
// };
//HttpClient client = new HttpClient(new DebugHttpMessageHandler(handler));

// v2
//HttpClient client = new HttpClient(new DebugHttpMessageHandler(new HttpClientHandler()));

// v3
HttpClient client = new HttpClient(new DebugHttpMessageHandler());

client.DefaultRequestHeaders.UserAgent.ParseAdd("Duotify/1.0");

HttpResponseMessage response = await client.GetAsync("https://postman-echo.com/get");

string jsonText = await response.Content.ReadAsStringAsync();

這段程式在執行時,會在 Console 輸出類似以下的訊息:

Request: GET https://postman-echo.com/get
Headers: User-Agent: Duotify/1.0

Response: OK (200)
Headers: Date: Fri, 26 Apr 2024 15:58:41 GMT
Headers: Connection: keep-alive
Headers: ETag: W/"111-PmkfQe2oac4nQoCIljhYp5s+hVo"
Headers: Set-Cookie: sails.sid=s%3ASV_p307ADVhZpm0bAG0SVPu3HUrQbLHK.a0oT6kjU3jmleYeL5Fd5OJ1R7gIwLm6LnFz8O6NkmOw; Path=/; HttpOnly
Response Body:
{
  "args": {},
  "headers": {
    "x-forwarded-proto": "https",
    "x-forwarded-port": "443",
    "host": "postman-echo.com",
    "x-amzn-trace-id": "Root=1-662bcf31-6f8cc63d23fe53441c7c8d3d",
    "user-agent": "Duotify/1.0"
  },
  "url": "https://postman-echo.com/get"
}

這樣就可以在程式執行時,看到 HttpClient 發出的所有 HTTP Request 與 Response 資料了,是不是相當方便!😊

使用範例:以 ASP.NET Core 應用程式為例

在 ASP.NET Core 裡面設定稍微有點不太一樣,因為要搭配 ASP.NET Core 的 DI 進行實作:

public class DebugHttpMessageHandler : DelegatingHandler
{
  private ILogger<DebugHttpMessageHandler> _logger;

  public DebugHttpMessageHandler(ILogger<DebugHttpMessageHandler> logger) : base(new HttpClientHandler())
  {
    this._logger = logger;
  }

  public DebugHttpMessageHandler(HttpMessageHandler handler, ILogger<DebugHttpMessageHandler> logger) : base(handler)
  {
    this._logger = logger;
  }

  protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
  {
    // Log request method and URL
    _logger.LogInformation("Request: {0} {1}", request.Method, request.RequestUri);

    // Log headers
    foreach (var header in request.Headers)
    {
      Console.WriteLine("Default Headers: {0}: {1}", header.Key, string.Join(", ", header.Value));
    }

    foreach (var header in request.Content.Headers)
    {
      Console.WriteLine("Content Headers: {0}: {1}", header.Key, string.Join(", ", header.Value));
    }

    // Show request payload
    if (request.Content != null)
    {
      string payload = await request.Content.ReadAsStringAsync();
      _logger.LogInformation("Payload: {0}", payload);
    }

    //_logger.LogInformation();

    // Send the request and get the response
    HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

    // Log response status
    _logger.LogInformation("Response: {0} ({1})", response.StatusCode, ((int)response.StatusCode));

    // Log headers
    foreach (var header in response.Headers)
    {
      _logger.LogInformation("Headers: {0}: {1}", header.Key, string.Join(", ", header.Value));
    }

    string jsonText = await response.Content.ReadAsStringAsync();
    _logger.LogInformation("Response Body:\n{0}", jsonText);

    return response;
  }
}

然後在 Program.cs 中註冊 DebugHttpMessageHandler 與設定 IHttpClientFactory

// 一定要設定 Scoped,否則會有問題
builder.Services.AddScoped<DebugHttpMessageHandler>();

builder.Services.AddHttpClient("Debug")
    .ConfigurePrimaryHttpMessageHandler<DebugHttpMessageHandler>();

最後在 Controller 中使用 IHttpClientFactory 來取得 HttpClient 實例:

private HttpClient http;
private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger, IHttpClientFactory hf)
{
    http = hf.CreateClient("Debug");
    _logger = logger;
}

總結

這篇文章介紹了如何使用 DelegatingHandler 類別來攔截 HttpClient 發出的所有 HTTP Request 與 Response 資料,這個方法可以讓我們在開發時更容易了解 HttpClient 的實際傳輸內容,這對於除錯與開發都是相當有幫助的。

實務上,其實 DelegatingHandler 的應用還蠻廣的,針對一些攔截或追蹤的目的,都可以非常方便的透過這個方法來實作。例如:你可以透過自訂的 HttpMessageHandler 來實作一個簡單的 Retry Policy、實作一個簡單的 Cache Policy、自動加入 Authentication 或其他自訂的 HTTP Header 等等,都是非常方便的。

相關連結

留言評論