谈谈 Nginx 的 HTTP/2 POST Bug

几个月前,我发现在某些情况下,使用 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--

专题「HTTP/2 相关」的其他文章 »

Comments