Skip to content

Latest commit

 

History

History
251 lines (189 loc) · 9.84 KB

File metadata and controls

251 lines (189 loc) · 9.84 KB

HTTP Request Lab — 測試結果與發現

測試日期:2026-04-07 ~ 2026-04-08 測試環境:Docker containers(macOS host)

1. 專案起源

某個專案使用 Spring RestTemplate + Apache HttpClient 5.3.1 發送 POST JSON 請求, server 端收到的 body 為空 {},導致業務邏輯錯誤。

根因是 Apache HttpClient 5.3.1 預設使用 Transfer-Encoding: chunked, 而 server 端不支援 chunked decoding。

為了系統性地比較各語言、各 library 的 HTTP 請求行為差異,建立了此測試環境。

2. 測試範圍

共測試 6 種語言、21 個 HTTP library,每個 library 執行 5 種測試 × HTTP/HTTPS

語言 Library 版本
Python urllib (built-in) Python 3.12
Python requests 2.32.3
Python httpx 0.28.1
Python urllib3 2.3.0
Python aiohttp 3.11.14
Java HttpURLConnection (built-in) Java 17
Java java.net.http.HttpClient (built-in) Java 17
Java OkHttp 4.12.0
Spring RestTemplate + JDK Spring Boot 3.4.4
Spring RestTemplate + Apache HttpClient 5.4.2
Spring WebClient (Reactor Netty) 1.2.4
Go net/http (built-in) Go 1.22
Go Resty 2.16.5
Node.js http (built-in) Node.js 22
Node.js axios 1.14.0
Node.js node-fetch 3.3.2
Node.js got 14.4.0
Node.js undici 7.0.0
C# HttpClient (built-in) .NET 8
C# RestSharp 112.1.0

測試項目

測試 說明
GET Query string encoding 差異
POST JSON Content-Type、Content-Length vs chunked
POST Form URL encoding 差異
POST Multipart boundary 格式、file 編碼方式
Custom Headers 自動附加的 header 差異

3. 核心發現:Chunked Encoding

結論:所有 library 在 body 大小已知時,均使用 Content-Length

Transfer-Encoding: chunked → 0 個 library
Content-Length              → 21 個 library(全部)

該 chunked 問題已在 Apache HttpClient 5.4.2 修復。

版本 行為
Apache HttpClient 5.3.1(舊版) Transfer-Encoding: chunked — body 被吃掉
Apache HttpClient 5.4.2(我們測試) Content-Length: 144 — 正常

什麼時候會出現 chunked?

Chunked encoding 在以下情境可能出現:

  • Body 以 streaming 方式產生(大小事先未知)
  • 大檔案上傳(避免先讀整個檔案進記憶體)
  • Library 的 bug(如 Apache HttpClient 5.3.1)

4. HTTPS / TLS 差異

TLS 版本與加密套件

所有 21 個 library 都使用 TLS 1.3 + TLS_AES_128_GCM_SHA256。

這代表在現代版本的 runtime(Java 17、Python 3.12、Go 1.22、Node.js 22、.NET 8)下, TLS 的行為已經高度統一,不需要擔心版本差異。

SNI(Server Name Indication)差異

SNI 是 TLS handshake 時 client 告知 server「我要連哪個主機」的機制。 部分 Java 系 library 沒有設定 SNI:

SNI 設定 Library
有設定 server Python 全部、Node.js 全部、Go 全部、C# 全部
未設定(空值) Java HttpURLConnection、Java OkHttp、Java Apache HttpClient、Spring 全部

這重要嗎?

在多數情況下不影響功能(因為 server 只有一張憑證)。但在以下場景會出問題:

  • CDN / 反向代理:同一個 IP 服務多個域名,靠 SNI 選擇憑證
  • WAF / 防火牆規則:某些規則依賴 SNI 來判斷目標
  • 憑證驗證:嚴格的 server 可能要求 SNI 與憑證的 CN/SAN 匹配

Java 系的 SNI 缺失是因為我們使用自訂 SSLContext 配合自簽 CA 時, 某些 library 的實作沒有自動帶入 hostname 作為 SNI。 這在正式環境中使用公開 CA 時通常不會發生。

5. User-Agent 比較

各 library 的 User-Agent 格式差異明顯,可用來快速辨識請求來源:

Library User-Agent
Python urllib Python-urllib/3.12
Python requests python-requests/2.32.3
Python httpx python-httpx/0.28.1
Python urllib3 python-urllib3/2.3.0
Python aiohttp Python/3.12 aiohttp/3.11.14
Java HttpURLConnection Java/17.0.18
Java HttpClient Java-http-client/17.0.18
Java OkHttp okhttp/4.12.0
Spring RestTemplate (JDK) Java/17.0.18
Spring RestTemplate (Apache) Apache-HttpClient/5.4.2 (Java/17.0.18)
Spring WebClient ReactorNetty/1.2.4
Go net/http Go-http-client/1.1
Go Resty go-resty/2.16.5 (https://github.com/go-resty/resty)
Node.js http (不設定)
Node.js axios axios/1.14.0
Node.js node-fetch node-fetch
Node.js got got (https://github.com/sindresorhus/got)
Node.js undici (不設定)
C# HttpClient (不設定)
C# RestSharp RestSharp/112.1.0.0

觀察

  • 不設定 User-Agent 的 library:Node.js http、undici、C# HttpClient — 這 3 個都是「最底層」的實作
  • 帶版本號的:多數 library 會在 User-Agent 中暴露版本號,可能有安全顧慮
  • Spring RestTemplate 的 User-Agent 取決於底層 library:用 JDK 就是 Java/17,用 Apache 就是 Apache-HttpClient/5.4.2

6. 自動附加的 Header 數量

各 library 會自動加上不同數量的 header,數量越多代表 library 做了越多「隱性行為」:

Library Header 數量 說明
Node.js http 6 最精簡
Node.js undici 6 同為底層實作
Go net/http 7
Python urllib3 7
Python urllib 8
Python aiohttp 8
Java HttpURLConnection 8
Java OkHttp 8
Spring RestTemplate (JDK) 8
Spring WebClient 8
Go Resty 8
Node.js got 8
Python requests 9
Python httpx 9
Java HttpClient 9 含 HTTP/2 升級 header
Spring RestTemplate (Apache) 9
Node.js axios 9
Node.js node-fetch 9
C# HttpClient 11
C# RestSharp 14 最多,含大量 Accept 類型

觀察

  • 最精簡(6 個):Node.js http 和 undici — 只送必要的 header
  • 最豐富(14 個):C# RestSharp — 自動加上大量的 Accept 類型
  • Java HttpClient 特殊:自動帶 Upgrade: h2cHTTP2-Settings,嘗試升級到 HTTP/2

7. Header 大小寫

HTTP 規範中 header name 是 case-insensitive,但各 library 的實作不同:

風格 Library
首字母大寫(Content-Type 多數 library
全小寫(content-type Node.js got、node-fetch、undici、Spring WebClient (Netty)

Spring WebClient 底層使用 Netty,Netty 按照 HTTP/2 的慣例使用全小寫 header name。 如果 server 端用 case-sensitive 的方式比對 header name,可能會出問題。

8. JSON Body 格式差異

同樣的 JSON 資料,各語言的序列化結果不同:

特徵 Library
緊湊格式(無空格) Java、Go、Node.js、Python
格式化格式(有縮排) C# HttpClient、C# RestSharp
欄位順序改變 Go net/http(map 遍歷順序隨機)

C# 預設使用格式化的 JSON 輸出(有換行和縮排),導致 Content-Length 較大(189 vs 144 bytes)。

9. Multipart Boundary 格式差異

各 library 產生的 multipart boundary 格式差異很大:

類型 Library Boundary 範例
固定字串 Node.js 全部(手動實作) ----NodejsAxiosBoundary
固定前綴 + 隨機數字 Java HttpURLConnection、Java HttpClient ----JavaHttpClient1775618784560
UUID 格式(帶引號) Apache HttpClient、Spring "0a901258-3c50-4eb6-a3c5-0357b1c9781f"
十六進位亂數 Python requests、urllib3 5133bbf2b955429b8752b38a33be967b
長十六進位亂數 Python httpx、aiohttp 2951533d1e6b047d9a0819d40f9ca9282ae682eac7c08413b447682a02fe
固定字串 Python urllib(手動實作) ----PythonUrllibBoundary

觀察

  • 帶引號的 boundary:Apache HttpClient 和 Spring 的 boundary 用雙引號包裹,某些舊版 server 可能無法正確解析
  • boundary 長度差異大:從 24 字元到 60 字元不等
  • 隨機性:每次請求的 boundary 都不同(除了手動實作的固定字串)

10. HTTP/2 升級行為

Java 11+ 的 java.net.http.HttpClient 會自動嘗試 HTTP/2 升級:

Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA

這是 HTTP/2 over cleartext (h2c) 的升級機制。其他 library 都沒有這個行為。

注意:這只出現在 HTTP(非加密)連線中。HTTPS 連線會透過 TLS ALPN 協商 HTTP/2, 不需要這些額外的 header。

11. 結論與建議

給開發者

  1. 升級 Apache HttpClient:5.3.1 有 chunked encoding 的 bug,至少升級到 5.4.2
  2. 注意 User-Agent 暴露的資訊:版本號可能被用於攻擊已知漏洞
  3. Header 大小寫不要假設:server 端應該用 case-insensitive 比對
  4. JSON 序列化格式統一:C# 預設有縮排,跨語言對接時注意 Content-Length 差異
  5. Multipart boundary 格式要容錯:特別注意帶引號的 boundary

給 Server 端

  1. 必須支援 chunked decoding:即使目前所有 library 都用 Content-Length,未來某個版本可能改變
  2. Body 驗證不要只靠 Content-Length:讀完整個 body 後再解析,不要提前中斷
  3. Log 要記錄完整的 request header:問題排查時,User-Agent 和 Transfer-Encoding 是最重要的線索
  4. SNI 不要作為安全判斷依據:Java 系 library 在使用自訂 SSL 時可能不帶 SNI

關於 TLS

目前所有現代 runtime 都統一使用 TLS 1.3 + AES-128-GCM。 TLS 版本差異在這一代已經不是問題,但如果對接的是舊系統(Java 8、Python 2、.NET Framework), 仍需注意 TLS 1.0/1.1 的相容性。