谈谈 Nginx 的 HTTP/2 POST Bug
<p>大概在三个月前,我发现在某些情况下,使用 Safari 无法登录我的博客后台。当时研究了一下,发现是 Nginx 处理 HTTP/2 POST 请求的一个 Bug。随后发布的 Nginx 1.11.0 解决了这个 Bug,我就没有持续再关注。直到今天看到 <a href="/misc/goto?guid=4959677000383316807" rel="nofollow,noindex">v2ex 这个帖子</a> ,我才发现 Nginx 并不打算把这部分代码合并到当前稳定版中。 如果你在使用 Nginx 1.10.x 部署 HTTP/2 服务,请务必看完本文 。</p> <h3>Bug 复现</h3> <p>复现这个 Bug 需要同时满足以下几个条件:</p> <ul> <li>使用 Nginx 最新稳定版(当前为 1.10.1)部署 HTTP/2 服务;</li> <li>使用特定的 HTTP/2 客户端,例如 OSX/iOS Safari、iOS 客户端、OkHttp 等(Chrome 没问题,MS IE/Edge 据说也有问题,但我没测试);</li> <li>只有 POST 场景才有问题(也就是说必须存在 DATA 帧);</li> <li>需要在建立 HTTP/2 连接后立即 POST(例如打开表单页面,再断网重连或重启 Nginx,不刷新页面直接提交);</li> </ul> <p>我用 OSX 10.11.6 自带的 Safari 9.1.2 可以稳定复现这个 Bug。触发 Bug 后,Safari 会提示无法连接到服务器。如下图:</p> <p><img src="https://simg.open-open.com/show/51452080fa06dc2b8d3335e542f857cc.gif"></p> <p>如果事先开启了 Nginx 的 debug 日志,可以找到类似这样的记录:</p> <pre> client sent stream with data before settings were acknowledged while processing HTTP/2 connection</pre> <h3>产生原因</h3> <p>为了减少网络时延,不少 HTTP/2 客户端会在建立 HTTP/2 连接时同时发送其它帧,包括用来 POST 数据的 DATA 帧。而 Nginx 在客户端接受到 SETTINGS 帧之前,一直将初始窗口大小(initial window size)设置为 0。也就是说,客户端收到 SETTINGS 帧之前发送的 DATA 帧,会被 Nginx 以 REFUSED_STREAM 帧拒绝。而部分客户端在收到 REFUSED_STREAM 帧之后,会提示连接失败,而不是发起重试,这就是产生 Bug 的原因。</p> <p>那么,Nginx 这个逻辑合理吗,客户端提前发送 DATA 帧符合 HTTP/2 协议规定吗?HTTP/2 协议中的「HTTP/2 Connection Preface」章节有以下描述:</p> <p>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. <a href="/misc/goto?guid=4959677000468554698" rel="nofollow,noindex">via</a></p> <p>出于减少时延的目的,HTTP/2 协议允许客户端在发送连接序言(connection preface)之后,立即发送其它帧,无需等待来自服务端的 SETTTINGS 帧。</p> <p>而 Nginx 能够正常处理客户端提前发送的其它帧,唯独 DATA 帧不行。因为客户端尚未收到 SETTINGS 帧之前,Nginx 将初始窗口大小设置为 0。</p> <p>那么 Nginx 的初始窗口大小应该设置为多少才合理呢?以下这段内容来自于 HTTP/2 协议的「Initial Flow-Control Window Size」章节:</p> <p>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. <a href="/misc/goto?guid=4959677000552016744" rel="nofollow,noindex">via</a></p> <p>也就是说 Nginx 应该将默认的初始窗口大小设置为 64KB。</p> <h3>如何解决</h3> <p>我猜测 Nginx 这么做是为了减少被攻击的风险,但无论如何这不符合 HTTP/2 协议规定,也造成了特定场景下 POST 请求不可用。Nginx 在 1.11.0 中解决了这一问题,并增加了一个配置项:</p> <p>Syntax: http2_body_preread_size size;</p> <p>Default: http2_body_preread_size 64k;</p> <p>Context: http, server</p> <p>This directive appeared in version 1.11.0.</p> <p>Sets the size of the buffer per each request in which the request body may be saved before it is started to be processed. <a href="/misc/goto?guid=4959677000635026736" rel="nofollow,noindex">via</a></p> <p>http2_body_preread_size 用来定义 Nginx 在客户端收到 SETTINGS 帧之前可以接受多大的 DATA 帧,默认为 64KB。如果将这个值设置为 0,那就跟之前版本的 Nginx 变得一样。</p> <p>需要特别注意的是, 这个改动不会被移植到 Nginx 当前稳定版中,也就是对于 Nginx 1.10.x,这个问题将始终存在 。对此,Nginx 有如下说明:</p> <p>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. <a href="/misc/goto?guid=4959677000713130615" rel="nofollow,noindex">via</a></p> <p>简而言之,Nginx 认为 HTTP/2 功能本身尚未稳定,要部署 HTTP/2 就应该使用 Nginx 主线版,而不是稳定版。从更新日志来看,Nginx 最近几个主线版本也一直在修复与 HTTP/2 有关的问题,印证了这一说法。</p> <p>HTTP/2 是一项年轻的技术,也是 HTTP 历史上最大的一次革新。在实践 HTTP/2 过程中,一定要有时刻踩坑的心理准备,更要时刻关注 HTTP/2 协议和实现者的最新动态。对于重要业务,一定要在充分测试和评估之后再推进 HTTP/2。</p> <p>本文链接: <a href="/misc/goto?guid=4959677000796312777" rel="nofollow,noindex">https://imququ.com/post/nginx-http2-post-bug.html</a> , <a href="/misc/goto?guid=4959677000875470360" rel="nofollow,noindex">参与评论 »</a></p> <p>-- EOF --</p> <p>发表于 2016-08-20 06:03:03 ,并被添加「Nginx、HTTP/2」标签。</p> <p> </p> <p>来自:https://imququ.com/post/nginx-http2-post-bug.html</p> <p> </p>
本文由用户 dengw2014 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
转载本站原创文章,请注明出处,并保留原始链接、图片水印。
本站是一个以用户分享为主的开源技术平台,欢迎各类分享!