談談 Nginx 的 HTTP/2 POST Bug

dengw2014 8年前發布 | 13K 次閱讀 Nginx HTTP Web服務器

大概在三個月前,我發現在某些情況下,使用 Safari 無法登錄我的博客后臺。當時研究了一下,發現是 Nginx 處理 HTTP/2 POST 請求的一個 Bug。隨后發布的 Nginx 1.11.0 解決了這個 Bug,我就沒有持續再關注。直到今天看到 v2ex 這個帖子 ,我才發現 Nginx 并不打算把這部分代碼合并到當前穩定版中。 如果你在使用 Nginx 1.10.x 部署 HTTP/2 服務,請務必看完本文 。

Bug 復現

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

  • 使用 Nginx 最新穩定版(當前為 1.10.1)部署 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 的 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 變得一樣。

需要特別注意的是, 這個改動不會被移植到 Nginx 當前穩定版中,也就是對于 Nginx 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。

本文鏈接: https://imququ.com/post/nginx-http2-post-bug.html參與評論 ?

-- EOF --

發表于 2016-08-20 06:03:03 ,并被添加「Nginx、HTTP/2」標簽。

 

來自:https://imququ.com/post/nginx-http2-post-bug.html

 

 本文由用戶 dengw2014 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!