1. 程式人生 > >談談 Nginx 的 HTTP/2 POST Bug

談談 Nginx 的 HTTP/2 POST Bug

文章目錄

提醒:本文最後更新於 774 天前,文中所描述的資訊可能已發生改變,請謹慎使用。

幾個月前,我發現在某些情況下,使用 Safari 無法登入我的部落格後臺。當時研究了一下,發現這是 Nginx 處理 HTTP/2 POST 請求的一個 Bug。由於隨後釋出的 Nginx 1.11.0 修復了這個 Bug,我沒有再持續關注。直到今天看到 v2ex 這個帖子,我才發現 Nginx 並沒有將修復程式碼合併到當前穩定版中。所以,如果你在使用 Nginx 1.9.15~1.10.x 部署 HTTP/2 服務,請務必看完本文

Bug 復現

復現這個 Bug 需要同時滿足以下幾個條件:

  • 使用 Nginx 特定版本(1.9.15~1.10.x)部署 HTTP/2 服務;
  • 使用特定的 HTTP/2 客戶端,例如 OSX/iOS Safari、iOS 客戶端、OkHttp 等(Chrome 沒問題,MS IE/Edge 據說也有問題,但我沒測試);
  • 只有 POST 場景才有問題(也就是說必須存在 DATA 幀);
  • 需要在建立 HTTP/2 連線後立即 POST(例如開啟表單頁面,再斷網重連或重啟 Nginx,不重新整理頁面直接提交);

我用 OSX 10.11.6 自帶的 Safari 9.1.2 可以穩定復現這個 Bug。觸發 Bug 後,Safari 會提示無法連線到伺服器。如下圖:

nginx http/2 post bug

如果事先開啟了 Nginx 的 debug 日誌,可以找到類似這樣的記錄:

client sent stream with data before settings were acknowledged while processing HTTP/2 connection

產生原因

為了減少網路時延,不少 HTTP/2 客戶端會在建立 HTTP/2 連線時同時傳送其它幀,包括用來 POST 資料的 DATA 幀。而 Nginx 在客戶端接受到 SETTINGS 幀之前,一直將初始視窗大小(initial window size)設定為 0。也就是說,客戶端收到 SETTINGS 幀之前傳送的 DATA 幀,會被 Nginx 以 REFUSED_STREAM 幀拒絕。而部分客戶端在收到 REFUSED_STREAM 幀之後,會提示連線失敗,而不是發起重試,這就是產生 Bug 的原因。

那麼,Nginx 這個邏輯合理嗎,客戶端提前傳送 DATA 幀符合 HTTP/2 協議規定嗎?HTTP/2 協議中的「HTTP/2 Connection Preface」章節有以下描述:

To avoid unnecessary latency, clients are permitted to send additional frames to the server immediately after sending the client connection preface, without waiting to receive the server connection preface. It is important to note, however, that the server connection preface SETTINGS frame might include parameters that necessarily alter how a client is expected to communicate with the server. Upon receiving the SETTINGS frame, the client is expected to honor any parameters established. In some configurations, it is possible for the server to transmit SETTINGS before the client sends additional frames, providing an opportunity to avoid this issue. via

出於減少時延的目的,HTTP/2 協議允許客戶端在傳送連線序言(connection preface)之後,立即傳送其它幀,無需等待來自服務端的 SETTTINGS 幀。

而 Nginx 能夠正常處理客戶端提前傳送的其它幀,唯獨 DATA 幀不行。因為客戶端尚未收到 SETTINGS 幀之前,Nginx 將初始視窗大小設定為 0。

那麼 Nginx 的初始視窗大小應該設定為多少才合理呢?以下這段內容來自於 HTTP/2 協議的「Initial Flow-Control Window Size」章節:

Prior to receiving a SETTINGS frame that sets a value for SETTINGS_INITIAL_WINDOW_SIZE, an endpoint can only use the default initial window size when sending flow-controlled frames. Similarly, the connection flow-control window is set to the default initial window size until a WINDOW_UPDATE frame is received. via

也就是說 Nginx 應該將預設的初始視窗大小設定為 64KB。

如何解決

我猜測 Nginx 這麼做是為了減少被攻擊的風險,但無論如何這不符合 HTTP/2 協議規定,也造成了特定場景下 POST 請求不可用。Nginx 在 1.11.0 中解決了這一問題,並增加了一個配置項:

Syntax: http2_body_preread_size size;
Default: http2_body_preread_size 64k;
Context: http, server
This directive appeared in version 1.11.0.

Sets the size of the buffer per each request in which the request body may be saved before it is started to be processed. via

http2_body_preread_size 用來定義 Nginx 在客戶端收到 SETTINGS 幀之前可以接受多大的 DATA 幀,預設為 64KB。如果將這個值設定為 0,那就跟之前版本的 Nginx 變得一樣。

需要特別注意的是,這個 Bug 由 Nginx 1.9.15 引入,而官方表示修復方案不會被移植到當前穩定版中,也就是對於 Nginx 1.9.15~1.10.x,這個問題將始終存在。對此,Nginx 有如下解釋:

We don't backport features to the stable branch (that's what we call stable, no enhancements). It receives only critical bug fixes. If you use such new, very complicated and actively developing protocol as HTTP/2 then it's naturally that you have to stay with the mainline branch. via

簡而言之,Nginx 認為 HTTP/2 功能本身尚未穩定,要部署 HTTP/2 就應該使用 Nginx 主線版,而不是穩定版。從更新日誌來看,Nginx 最近幾個主線版本也一直在修復與 HTTP/2 有關的問題,印證了這一說法。

HTTP/2 是一項年輕的技術,也是 HTTP 歷史上最大的一次革新。在實踐 HTTP/2 過程中,一定要有時刻踩坑的心理準備,更要時刻關注 HTTP/2 協議和實現者的最新動態。對於重要業務,一定要在充分測試和評估之後再推進 HTTP/2。

Update @ 2016.10.21,Nginx 最終還是在 1.10.2 也修復了這個 Bug,使用穩定版的同學可以考慮升級了。感謝 @ZE3kr 的反饋。

--EOF--

提醒:本文最後更新於 774 天前,文中所描述的資訊可能已發生改變,請謹慎使用。