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

在 .NET 要呼叫 REST API 時，最常使用的就屬 [HttpClient](https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.httpclient?view=net-8.0&WT.mc_id=DT-MVP-4015686) 類別莫屬，但有時候真的想看他到底送出了什麼 HTTP 封包，或是收到了什麼 HTTP 回應 (原始內容)，這時候就需要一個方法來攔截並記錄實際 HTTP 傳送的 Request 與 Response 封包內容。其實是有方法的，這篇文章就來介紹如何實作。

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/67093959559dadb023ad-be9d850c-05f6-423e-a82d-d07f403e7f2b.webp)

### 使用 `DelegatingHandler` 類別

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

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

```csharp
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` 實例，以下是一個使用範例：

```csharp
// 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 進行實作：

```csharp
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`：

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

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

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

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

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

### 總結

這篇文章介紹了如何使用 [`DelegatingHandler`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler?view=net-8.0&WT.mc_id=DT-MVP-4015686) 類別來攔截 `HttpClient` 發出的所有 HTTP Request 與 Response 資料，這個方法可以讓我們在開發時更容易了解 `HttpClient` 的實際傳輸內容，這對於除錯與開發都是相當有幫助的。

實務上，其實 [`DelegatingHandler`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.delegatinghandler?view=net-8.0&WT.mc_id=DT-MVP-4015686) 的應用還蠻廣的，針對一些攔截或追蹤的目的，都可以非常方便的透過這個方法來實作。例如：你可以透過自訂的 `HttpMessageHandler` 來實作一個簡單的 Retry Policy、實作一個簡單的 Cache Policy、自動加入 [`Authentication`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) 或其他自訂的 HTTP Header 等等，都是非常方便的。

### 相關連結

-   [HttpClient Class (System.Net.Http) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-8.0&WT.mc_id=DT-MVP-4015686)
-   [HttpClient and HttpMessageHandler in .NET 8 - The Harmony of HTTP Communication](https://www.linkedin.com/pulse/httpclient-httpmessagehandler-net-8-harmony-http-communication-m-emmge/)
-   [Make HTTP requests using IHttpClientFactory in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-8.0&WT.mc_id=DT-MVP-4015686#configure-the-httpmessagehandler)
