一次不规范 HTTP 请求引发的 nginx 响应 400 问题分析与解决
背景
最近分析数据偶然发现 nginx log 中有一批用户所有的 HTTP POST log 上报请求均返回 400,没有任何 200 成功记录,由于只占整体请求的不到 0.5%,所以之前也一直没有触发监控报警,而且很奇怪的是只对于 log 上报的 POST 接口会存在这种特定用户全部 400 的情况,而对于其他接口无论 POST 还是 GET 均没有此类问题。
进一步分析 log 发现其实对某些地区的用户请求,这个比例甚至超过了 10%,于是花时间跟进了一下,最终发现源于部分机型客户端发出的 HTTP 请求格式不规范导致,这里记录一下分析过程、原因以及最终解决方案。
问题分析
常见 nginx 400 原因
搜寻网上资料,发现一般可能有以下几个原因会导致 nginx 响应 400:
request_uri 过长超过 nginx 配置大小
cookie 或者 header 过大超过 nginx 配置大小
空 HOST 头
content_length 和 body 长度不一致
这些错误其实都是发生在 nginx 这一层,即 nginx 处理时认为客户端请求格式错误,于是直接返回 400,不会向 upstream server 转发请求,因而 upstream server 对这些错误请求其实完全是无感知的。
而这次根据 nginx log 分析,可以看到 nginx 其实有向 upstream server 转发请求--upstream_addr 已经是 upstream server 有效地址,所以 400 实际应当是 upstream server 返回的,而不是 nginx 直接返回,这说明至少 nginx 这一层认为请求格式是没问题的。
实际 nginx 400 log 分析
截取部分线上部分用户的错误日志,其大体样式如下
日志分析可以发现大部分 400 请求都有一个问题:其 query 参数并未经过 urlencode,比如可以很明显看到其参数 channel=Google Play 中的空格并未转码成 %20,直觉上推断这应该和 400 的原因有直接关系。
试错
为了验证未转码 query 参数是否是导致 400 的直接原因,简单通过 curl 构造几个测试 http 请求:
发现凡是带空格的请求 upstream server 均会直接返回 400,这里可以推断 query 参数未 urlencode 是 400 问题的直接原因了,但是为什么未转码会导致 400 呢?怎么从 HTTP 原理上解释这个现象?为了找到答案,需要回顾了一下 HTTP 协议标准。
HTTP 请求规范格式
HTTP 的请求消息格式如下:
如上图所示,作为一种文本协议,对 HTTP 请求消息中不同部分的区别、拆分完全是基于空格 、回车符\r、换行符\n 这些字符标记进行的,对于第一行的三个部分请求方法、URL 和协议版本的拆分即是根据空格进行 split。
分析查到的 400 HTTP 请求,可以发现由于 query 参数未 urlencode,导致其中会出现空格,这时严格来说这个请求已经不符合 HTTP 规范了,因为此时第一行再根据空格可以 split 出超过 3 部分,无法与 method、URL、version 再一一对应,从语义上来说此时直接返回 400 是合理处理逻辑。
实际处理中,面对这种情况,有的组件能兼容处理--把 split 的首部和尾部分别作为 method 与 version,而中间剩余部分统一作为 URL,比如 nginx 即兼容了这种不规范格式,但是很多组件并不能兼容处理这种情况--毕竟这并不符合 HTTP 规范,比如 charles 抓包此种请求会出错、golang 的 net/http 库、Django 的 http 模块收到这类请求都会报 400...
golang net/http 解析 HTTP 代码分析
负责日志上报的 upstream server 是 golang 实现的 logsvc,其使用标准卡库 net/http 处理 HTTP 请求,进一步探究一下该标准库是怎么解析 HTTP 请求的,以确认错误原因。
根据 golang 源码,可以发现其 HTTP 请求解析的路径为 http.ListenAndServe => http.Serve => serve => readRequest.... 其解析 HTTP 请求头的逻辑即位于 readRequest 函数中。
readRequest 部分代码如下:
可以看到 readRequest 中先通过 parseRequestLine 解析出首行的 method, URL 与 Proto 三个字段,然后通过 ParseHTTPVersion 解析 version 是否正确,不正确则报错{"malformed HTTP version", 最终会导致响应 400。
parseRequestLine 代码如下:
可以看到 parseRequestLine 的解析代码是通过查找第 0 个、第 1 个空格 index,然后直接基于 slice 语法将其切成了 method、requestURI、proto 三部分,如果 requestURI 中包含额外空格,会导致 proto 取值实际变为第一个空格之后的所有字符,比如"POST abc/?x=o space d HTTP/1.1"会被解析为:method=POST, requestURI=abc/?x=0, proto=" space d HTTP/1.1",这会导致下一步 ParseHTTPVersion 解析出错。
ParseHTTPVersion 代码如下,可以发现之前 parseRequestLine 解析得到的 version 字段如果不合法,则会返回错误:
解决方案
首先要做的是先和客户端对齐问题,客户端确认部分机型上其调用 unity 的网络库方法未能对其 query 参数正常 urlencode,新版本将在 unity 网络库之上增加额外代码保证所有参数必须 urlencode,使其符合 HTTP 规范。
而后进一步考虑可否先临时兼容处理线上已有的异常请求,防止新版本覆盖修复前这部分异常用户 log 上报数据的持续丢失,针对兼容考虑了以下几个方案
尝试三方 HTTP golang 库 gin && echo
由于日志服务由独立的 golang server 负责,其代码逻辑很简单:只是对 log 的 POST 请求的 body 进行解压缩、解析、写入 kafka,并无其他额外逻辑,改动成本较低,因此先考虑了替换 net/http 为其他三方库看是否能解决问题。
先后尝试了流行的 gin 和 echo 库发现都报 400,忍不住又探究了其源码,结果发现这两个库内部其实都调用了 net/http 的 ListenAndServer 和 Serve 方法,其前面的解析逻辑就是 net/http 对应代码负责的,因而自然也会报 400。
nginx lua/perl 脚本更改 query 参数
想到的另一个可能方法是在 nginx 层使用 lua/perl 脚本对传入的未 urlencode 的 request_uri 参数进行 urlencode 后再发给 upstream server,但是发现线上 nginx 编译时并未集成 lua、perl 的模块。要采用此种方法则只能:
要么重新编译整个 nginx 替换原 nginx
或者采用动态加载的方式单独编译 perl、lua 模块后使用 nginx 动态加载
考虑到本人作为 RD 而非专业 nginx OP 人员,和对线上影响的风险不轻易尝试。
nginx 将 log/report 路由至可兼容空格未转码 HTTP 请求的 server
开头提到过对于待空格的异常请求,只有 log 上报 POST 接口会返回 400,其他接口都返回正常,这其实是因为在 nginx 转发时对正常的业务接口和 log 接口进行了拆分,log/report 接口会单独转发到独立的 golang logsvc 服务,而正常业务请求均会转发给 python 的主 api 服务。
回顾当初之所以会拆分一个单独的 golang server 负责 app log 上报的解析和写 kafka,而不再和其他接口逻辑一样都由主 api 服务负责,主要是两个原因:
Pythono 写的 api 主服务相对效率较低,对于频繁、大量的 log 上报可能耗费过多资源,速度也较慢
避免 log 上报类请求影响其他正常的业务请求响应速度,将业务逻辑与日志上报两者解耦
当前 logsvc 无法处理的此种情况,使用 uwsgi 协议与 nginx 交互的 api 主服务却可以正常解析,因而在 nginx 添加如下临时配置:
即通过正则匹配 query 参数(args)中若不存在空格直接交由 logsvc 处理,存在空格则交由使用 uwsgi 协议的 api 主服务处理,由于此类异常请求仅占整体请求的不到 0.5%,之前考虑的拆分架构依然 work,只是对于少量的异常请求先通过 api 主服务进行兼容处理。
转载请注明出处,原文地址: https://www.cnblogs.com/AcAc-t/p/nginx_400_problem_for_not_encode_http_request.html
版权声明: 本文为 InfoQ 作者【高端章鱼哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/a4fc60f218da1e66157406b40】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论