﻿# HttpClient 的 PostAsJsonAsync 擴充方法如何停用 chunked 分段資料傳輸

由於 Azure Logic App 的 HTTP Trigger 不支援 `Transfer-Encoding: chunked` 的 HTTP 要求，而 HttpClient 的 [PostAsJsonAsync](https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.json.httpclientjsonextensions.postasjsonasync?view=net-5.0) 擴充方法只支援透過 `Transfer-Encoding: chunked` 發出 HTTP 要求，這個問題是我嘗試了將近 30 分鐘才發現的魔鬼細節，真的是雷到一整個無以復加。今天這篇文章，我要來說明如何讓 HttpClient 的 PostAsJsonAsync 擴充方法支援 chunked 資料傳輸，以及 `Transfer-Encoding: chunked` 這個 HTTP 標頭到底是什麼。

> 以下文章會不斷提到 `chunking` 或 `chunked transfer` 等專有名詞，其代表的是 HTTP 封包內容以「分段傳輸」的方式傳送資料，通常用於大量資料傳輸時，可以將資料一段一段的傳到遠端 HTTP 伺服器。而只要 HTTP 用戶端要送出 chunked 的分段資料，就會在 HTTP Request Headers 中加入 `Transfer-Encoding: chunked` 標頭。

### 問題描述

我原本想從 Azure Logic App 取得用戶端 IP 地址，然後自動將取得的 IP 加入 Azure 的**網路安全性群組**(NSG)，結果發現 Azure Logic App 目前完全無法取得用戶端 IP 地址，因此必須要透過 Azure Functions 先取得 IP 地址後，再將 IP 轉發到 Azure Logic App 的 [HTTP Trigger](https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-http-endpoint?WT.mc_id=DT-MVP-4015686) 觸發動作。

> 在 Azure 的 UesrVoice 網站有一條 [Add ability to see client IP Address (for HTTP triggers) in the job history and/or diagnostics logs](https://feedback.azure.com/forums/287593-logic-apps/suggestions/39780991-add-ability-to-see-client-ip-address-for-http-tri) 使用者建議，但看起來微軟還沒有關注到這個部分。

但是 Azure Logic App 的 [HTTP Trigger](https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-http-endpoint?WT.mc_id=DT-MVP-4015686) ([Request](https://learn.microsoft.com/en-us/azure/connectors/connectors-native-reqres?WT.mc_id=DT-MVP-4015686)) 不支援收到 `Transfer-Encoding: chunked` 要求。我再 [Handle large messages with chunking in Azure Logic Apps](https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-handle-large-messages#set-up-chunking-over-http?WT.mc_id=DT-MVP-4015686) 這份文件有看到一句話：

> Logic App triggers don't support **chunking** because of the increased overhead of exchanging multiple messages.

然而，我的 Azure Functions 應用程式是用 .NET Core 3.1 寫成的，很自然的我會用 [HttpClient](https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.httpclient?view=net-5.0) 來發出 HTTP 要求。而 .NET 5.0 內建的 [PostAsJsonAsync](https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.json.httpclientjsonextensions.postasjsonasync?view=net-5.0) 擴充方法，由於實在是太甜了，我幾乎所有需要 POST 發出 JSON 資料的 HTTP 要求，幾乎都會用這個 API 來呼叫。但是重點就是：「**這個 PostAsJsonAsync 預設所有發出的 HTTP 要求，全部都會用 `chunking` 的方式發出，以致於 Azure Logic App 可以收到要求，但是完全收不到 Request Body 的狀況。**」

> 在 Azure 的 UesrVoice 網站有一條 [Implement chunked transfer-encoding inside Logic Apps](https://feedback.azure.com/forums/287593-logic-apps/suggestions/33616297-implement-chunked-transfer-encoding-inside-logic-a) 使用者建議，但看起來微軟也還沒有關注到這個部分。

其實這個狀況真的很奇特，原因有幾點：

1.  所有瀏覽器預設都不會以 chunking 的方式進行傳輸，除非資料量特別大的情況，才會將 HTTP POST 的資料進行分段傳送。
2.  所有伺服器預設都可以支援 chunking 的方式接收要求，誰知道 Azure Logic App 不支援接收 chunking 要求。
3.  事實上 Azure Logic App 可以接收到 HTTP POST 要求，沒有任何錯誤訊息，但是收到的 `Content-Length` 永遠為 `0`，而且完全沒有 Request Body 會傳入，但我明明有送資料過去啊，這真的會偵錯到懷疑人生！
4.  然後我去 **.NET Runtime**(GitHub) 找到一條 [Issue #49357](https://github.com/dotnet/runtime/issues/49357) 提到跟我一樣的困難，但是這條最後是以**不處理**結案，原因是微軟的人回應說「**there is no detail about wide-spread server usage with chunked encoding disabled / not working**」(沒有已知的伺服器不支援 chunked encoding 被停用的情況)。可是瑞凡，不支援 chunked encoding 的就是你們家 Azure 的產品啊！(怒)

### 證明 HttpClient 的 [PostAsJsonAsync](https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.json.httpclientjsonextensions.postasjsonasync?view=net-5.0) 實作方式

首先，最簡單可以得知真相的方法，就是從 [HttpClientJsonExtensions.Post.cs](https://github.com/dotnet/runtime/blob/v5.0.8/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs) 原始碼去看 `PostAsJsonAsync` 的實作細節。

其次，我們也可以寫點 Code 來驗證一下：

1.  初始化專案
    
    ```bash
    mkdir TestPostAsJsonAsync
    cd TestPostAsJsonAsync
    dotnet new globaljson --sdk-version 3.1.100
    dotnet new console
    curl https://www.toptal.com/developers/gitignore/api/visualstudio > .gitignore
    git init
    git add .
    git commit -m "Initial commit"
    ```
    
2.  加入 [System.Net.Http.Json](https://www.nuget.org/packages/System.Net.Http.Json) 套件
    
    由於 .NET Core 3.1 還沒有內建 `PostAsJsonAsync` 擴充方法，因此只要額外加裝 [System.Net.Http.Json](https://www.nuget.org/packages/System.Net.Http.Json) 套件就可以使用！
    
    ```bash
    dotnet add package System.Net.Http.Json
    ```
    
3.  進入 [https://webhook.site/](https://webhook.site/) 網站取得 unique URL
    
    假設網址 `https://webhook.site/265de048-0b91-4d6b-be7d-16cbf209ff05` 是我們要測試發出要求的網址！
    
4.  加入 `HttpClient` 與 `PostAsJsonAsync` 的範例程式
    
    ```csharp
    using System.Net.Http;
    using System.Net.Http.Json;
    using System.Threading.Tasks;
    
    namespace TestPostAsJsonAsync
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                using var client = new HttpClient();
    
                await client.PostAsJsonAsync(
                    "https://webhook.site/265de048-0b91-4d6b-be7d-16cbf209ff05",
                    new { ClientIP = "127.0.0.1" });
            }
        }
    }
    ```
    
    ```bash
    dotnet run
    ```
    
5.  回到 Webhook.site 網站查看剛剛發出的 HTTP 封包內容
    
    此時你會驚人的發現，明明發出的 HTTP 封包內容很小，但還是看到了 `Transfer-Encoding: chunked` 這個 HTTP 標頭，這也意味著由 `PostAsJsonAsync` 發出的 HTTP 封包，永遠都會以 chunking 的方式傳送資料。
    
    ![Webhook.site - Test, process and transform emails and HTTP requests](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/49f085acaa4d26943fe8-125806870-af849dc4-fb9c-4a85-9a71-ee9f758427ef.jpg)
    

### 如何有效關閉 HttpClient 的 chunking 行為

我沒辦法左右 Azure Logic App 產品的設計，但我可以改變 .NET 用戶端的 HTTP 發送行為。

事實上，這個 `HttpClient` 並非只能發出 chunked 的 HTTP 封包，而是當發出去的 **內容** ([HttpContent](https://learn.microsoft.com/zh-tw/dotnet/api/system.net.http.httpcontent?view=net-5.0)) 當無法被事先判斷時，預設就會以 chunked 的方式進行傳輸，當達到預設的 Buffered Size 的時候，就會選擇先送出內容，然後在累積 JSON 序列化的內容，達到 Buffered Size 的時候，就再送出下一段內容。

我們可以從 [HttpClientJsonExtensions.Post.cs](https://github.com/dotnet/runtime/blob/v5.0.8/src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.Post.cs) 原始碼看到主要的程式碼是以下這段：

```csharp
JsonContent content = JsonContent.Create(value, mediaType: null, options);
return client.PostAsync(requestUri, content, cancellationToken);
```

在使用 `JsonContent.Create` 建議 `JsonContent` 的時候，其實含沒有對 `value` 進行序列化，而執行 `client.PostAsync` 的時候，才會邊序列化、邊傳送資料，以增加程式的執行效率。基本上，在透過 HTTP POST 傳輸大量內容的時候，使用 chunked transfer 可以大幅提昇效率！

簡單來說，要關閉 chunked transfer 的話，只要將 `JsonContent` 全部都先讀入**緩衝區**(Buffer)，之後傳給 `HttpClient` 的時候，就已經得知所有要傳送出去的 HTTP 封包內容，此時就會關閉 chunked transfer 行為。程式碼範例如下：

```csharp
var content = JsonContent.Create<T>(value);
await content.LoadIntoBufferAsync();
var response = await httpClient.PostAsync(requestUri, content);
```

所以我們剛剛的範例程式，只要改成以下，就可以完美關閉 chunked transfer 行為：

```csharp
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using System.Text.Json;

namespace TestPostAsJsonAsync
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using var client = new HttpClient();

            var content = JsonContent.Create(new { ClientIP = "127.0.0.1" });
            await content.LoadIntoBufferAsync();
            var response = await client.PostAsync(
                "https://webhook.site/265de048-0b91-4d6b-be7d-16cbf209ff05",
                content);
        }
    }
}
```

![2021-07-15\_23-10-57](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/bdb9454c1b5039fc95ed-125811949-45c70092-d686-4dfd-aaf3-af40327325a2.jpg)

### 相關連結

-   MSDN Forums > Azure Logic Apps > Does HTTP Request handle Tranfer-Encoding chunked ?
-   [Provide better json support for servers don't support chunked request body · Issue #49357 · dotnet/runtime](https://github.com/dotnet/runtime/issues/49357)
    -   [Provide better json support for servers don't support chunked request body (re-open) #55583](https://github.com/dotnet/runtime/issues/55583)
-   [c# - Where is the PostAsJsonAsync method in ASP.NET Core? - Stack Overflow](https://stackoverflow.com/a/62365042/910074)
-   [Get client IP address from Azure Logic app - Stack Overflow](https://stackoverflow.com/questions/66621423/get-client-ip-address-from-azure-logic-app)
-   [How to disable Chunked Transfer Encoding in ASP.Net C# using HttpClient - Stack Overflow](https://stackoverflow.com/a/51046655)
-   [Webhook.site](https://webhook.site/) - Test, process and transform emails and HTTP requests
