一个有趣的nginx HTTP 400响应问题分析

一个,有趣,nginx,http,响应,问题,分析 · 浏览次数 : 2943

小编点评

nginx 400 常量由于query参数中包含未转义空格导致状态机无法解析,所以需要解决该问题才能正常处理请求。 以下是一些解决方法: **1. 在query参数中添加\" H\"的组合** 例如: ``` nginx server { ... ... ... location = / { ... ... ... query = space; ... ... } ... ... } ``` **2. 在query参数中添加转义的空格** 例如: ``` nginx server { ... ... ... location = / { ... ... ... query = space; ... ... } ... ... } ``` **3. 在请求行中添加转义的空格** 例如: ``` nginx server { ... ... ... location = / { ... ... ... query = space; ... ... } ... ... ... request { ... ... ... add_space = space; ... ... } ... ... } ``` **4. 在nginx中使用状态机解析query参数** 例如: ``` nginx server { ... ... ... location = / { ... ... ... state = space; ... ... } ... ... ... location = / { ... ... ... state = space; ... ... } ... ... } ``` **5. 使用客户端进行urlencode** 例如: ``` client { ... ... ... request { ... ... ... add_space = space; ... ... } ... ... } ``` **6. 使用 upstream server处理请求** 例如: ``` upstream server { ... ... ... server { ... ... ... location = / { ... ... ... add_space = space; ... ... } } } ``` **7. 使用客户端进行urlencode并进行请求转发** 例如: ``` client { ... ... ... request { ... ... ... add_space = space; ... ... } ... ... ... upstream server { ... ... ... server { ... ... ... location = / { ... ... ... add_space = space; ... ... } } } ... ... } ``` 最终,选择哪种方法取决于你的需求和请求格式。

正文

背景

之前在一次不规范HTTP请求引发的nginx响应400问题分析与解决 中写过客户端query参数未urlencode导致的400问题,当时的结论是:

对于query参数带空格的请求,由于其不符合HTTP规范,golang的net/http库无法识别会直接报错400,而nginx和使用uwsgi与nginx交互的api主服务却可以兼容,可以正常处理。
最终的临时解决方案是:在nginx层根据query 参数是否包含空格决定是转发到golang的log server或api主服务。

本来以为这事就这么结束了,结果最近查询nginx的错误log,居然又发现少部分400错误,最终定位也是因为query 参数包含空格,而且这次报错是直接在nginx层返回400,后面的转发判定逻辑都不会被触发,于是很神奇的发现了两类空格导致的400问题:

  1. 第一类是之前解决了的nginx可以兼容识别,但golang 网络库无法识别会报400的含空格请求,举例入下:
curl 'http://test.myexample123.com/test?appname=demoapp&phonetype=android&device_type=android&osn=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&osv=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&channel=Google Play&model=HUAWEIHLK-AL00&build=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)'
{"status": 1, "data": {"test": "ok"}}
  1. 第二类是这次新发现的nginx层直接返回400的含空格请求,并且还发发现该类报错很多都是来源于华为手机,如下可看出其400响应为nginx直接返回:
curl 'http://test.myexample123.com/test?appname=demoapp&phonetype=android&device_type=android&osn=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&osv=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)&channel=Google Play&model=HUAWEI HLK-AL00&build=Android OS 10 / API-29 (HONORHLK-AL00/102.0.0.270C00)'
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx/1.16.1</center>
</body>
</html>

乍看之下其请求参数完全没看出区别,无论哪类问题只要去掉了空格就不会有问题了,难不成nginx对华为手机还有歧视不成(>_<)。

问题定位

两类问题都是由于query参数带空格引起的,最终通过二分法试错确认了其关键区别:如果query参数中包含" H"--即空格+H的组合,nginx层即会直接报错返回400,而如果不包含" H"这一组合,nginx层将能兼容处理--这解释了为何大部分400请求来自华为手机,因为华为手机model参数很多都是"HUAWEI HRY-AL00"这类取值,即包含了" H"这一子串,看起来" H"这个组合在nginx内部有特殊含义,华为手机给撞枪口上了。
那" H"在nginx中到底有什么特殊含义呢?又到了探究源码的时候了,通过拜读源码最终在ngx_http_parse.c 中负责解析http 请求行的ngx_http_parse_request_line 函数中找到了原因,如下

103 ngx_int_t
 104 ngx_http_parse_request_line(ngx_http_request_t *r, ngx_buf_t *b)
 105 {
 106     u_char  c, ch, *p, *m;
 107     enum {
 108         sw_start = 0,
 109         sw_method,
 110         sw_spaces_before_uri,
 111         sw_schema,
 112         sw_schema_slash,
 113         sw_schema_slash_slash,
 114         sw_host_start,
 115         sw_host,
 116         sw_host_end,
 117         sw_host_ip_literal,
 118         sw_port,
 119         sw_host_http_09,
 120         sw_after_slash_in_uri,
 121         sw_check_uri,
 122         sw_check_uri_http_09,
 123         sw_uri,
 124         sw_http_09,
 125         sw_http_H,
 126         sw_http_HT,
 127         sw_http_HTT,
 128         sw_http_HTTP,
 129         sw_first_major_digit,
 130         sw_major_digit,
 131         sw_first_minor_digit,
 132         sw_minor_digit,
 133         sw_spaces_after_digit,
 134         sw_almost_done
 135     } state;
 136
 137     state = r->state;
 138
 139     for (p = b->pos; p < b->last; p++) {
 140         ch = *p;
 141
 142         switch (state) {
 143
 144         /* HTTP methods: GET, HEAD, POST */
 145         case sw_start:
 146             r->request_start = p;
 147
 148             if (ch == CR || ch == LF) {
 149                 break;
 150             }
 ...
 486         /* check "/.", "//", "%", and "\" (Win32) in URI */
 487         case sw_after_slash_in_uri:
 488
 489             if (usual[ch >> 5] & (1U << (ch & 0x1f))) {
 490                 state = sw_check_uri;
 491                 break;
 492             }
 493
 494             switch (ch) {
 495             case ' ':
 496                 r->uri_end = p;
 497                 state = sw_check_uri_http_09; 
 498                 break;
 499             case CR:
 500                 r->uri_end = p;
 501                 r->http_minor = 9;
 502                 state = sw_almost_done;
 503                 break;
 ...
 606         /* space+ after URI */
 607         case sw_check_uri_http_09:
 608             switch (ch) {
...
 618             case 'H':
 619                 r->http_protocol.data = p;
 620                 state = sw_http_H;
 621                 break;
 622             default:
 623                 r->space_in_uri = 1;
 624                 state = sw_check_uri;
 625                 p--;
 626                 break;
 627             }
 628             break;
 ...
 684         case sw_http_H:
 685             switch (ch) {
 686             case 'T':
 687                 state = sw_http_HT;
 688                 break;
 689             default:
 690                 return NGX_HTTP_PARSE_INVALID_REQUEST;
 691             }
 692             break;
 693
 694         case sw_http_HT:
 695             switch (ch) {
 696             case 'T':
 697                 state = sw_http_HTT;
 698                 break;
 699             default:
 700                 return NGX_HTTP_PARSE_INVALID_REQUEST;
 701             }
 702             break;
 703
 704         case sw_http_HTT:
 705             switch (ch) {
 706             case 'P':
 707                 state = sw_http_HTTP;
 708                 break;
 709             default:
 710                 return NGX_HTTP_PARSE_INVALID_REQUEST;
 711             }
 712             break;
 ...

如上ngx_http_parse_request_line函数解析请求行原理为通过for循环逐个遍历字符,内部使用大量switch语句实现了一个状态机进行解析。
当解析到sw_after_slash_in_uri分支的case ' '(495行)时,会设置状态state=sw_check_uri_http_09,而后在sw_check_uri_http_09分支的case 'H'(618行)设置state=sw_http_H,而sw_http_H其实是HTTP protocol的解析分支,其负责解析出类似HTTP/1.1 这样的内容,所以在分支sw_http_H(684行)其期待的正确字符应该是HTTP/1.1的 第二个字符T,而后进入case sw_http_HT期待解析HTTP/1.1的第三个字符T,以此类推最终逐个解析完成整个protocol字符串,但是在sw_http_H分支中若没有解析到期望的字符T,其默认行为就是直接返回NGX_HTTP_PARSE_INVALID_REQUEST,也就是400常量了。
简单来说,nginx在解析请求行时,若在query参数中遇到了" H"的组合会导致状态机认为已经进入protocol字段的解析分支,当碰到不识别的字符串则认为格式错误,会直接返回400,而如果query参数中虽然包含未转义空格但却没有" H"组合,nginx的这个请求行解析状态机倒还能够一定程度兼容此类错误,将请求正常转发给upstream server处理。
当然,无论nginx能不能兼容query参数未转义空格,最正确的做法还是客户端应该一开始就保证所有query参数都经过必要urlencode再进行使用,这样压根就不会有这么一堆幺蛾子。

转载请注明出处:https://www.cnblogs.com/AcAc-t/p/nginx_http_400_for_space_H.html

与一个有趣的nginx HTTP 400响应问题分析相似的内容:

一个有趣的nginx HTTP 400响应问题分析

背景 之前在一次不规范HTTP请求引发的nginx响应400问题分析与解决 中写过客户端query参数未urlencode导致的400问题,当时的结论是: 对于query参数带空格的请求,由于其不符合HTTP规范,golang的net/http库无法识别会直接报错400,而nginx和使用uwsgi

使用openresty替换线上nginx网关之openresty安装细节

# 背景 线上跑了多年的一个网关业务,随着部门的拆分,逐渐有了一个痛点。该网关业务主要处理app端请求,app端发起的请求,采用http协议,post方法,content-type采用`application/x-www-form-urlencoded`,表单中有一个固定的字段,叫功能号,即func

[转帖]聊一聊nginx中KeepAlive的设置

文章目录 问题分析为什么要有KeepAlive?TCP KeepAlive和HTTP的Keep-Alive是一样的吗?Nginx的TCP KeepAlive如何设置Apache中KeepAlive和KeepAliveTimeOut参考资料 问题 之前工作中遇到一个KeepAlive的问题,现在把它记

Nginx性能调优5招35式不可不知的策略实战

Nginx是一个高性能的HTTP和反向代理服务器,它在全球范围内被广泛使用,因其高性能、稳定性、丰富的功能以及低资源消耗而受到青睐。今天V哥从5个方面来介绍 Nginx 性能调优的具体策略,希望对兄弟们有帮助,废话不多说,马上开整。 1. 系统层面: 调整内核参数:例如,增加系统文件描述符的限制、T

[转帖]记一次压测引起的nginx负载均衡性能调优

https://xiaorui.cc/archives/3495 这边有个性能要求极高的api要上线,这个服务端是golang http模块实现的。在上线之前我们理所当然的要做压力测试。起初是 “小白同学” 起头进行压力测试,但当我看到那压力测试的结果时,我也是逗乐了。 现象是,直接访问Golang

[转帖]专注于GOLANG、PYTHON、DB、CLUSTER 记一次压测引起的nginx负载均衡性能调优

https://xiaorui.cc/archives/3495 rfyiamcool2016年6月26日 0 Comments 这边有个性能要求极高的api要上线,这个服务端是golang http模块实现的。在上线之前我们理所当然的要做压力测试。起初是 “小白同学” 起头进行压力测试,但当我看到

HTTPS下tomcat与nginx的前端性能比较

HTTPS下tomcat与nginx的前端性能比较 摘要 之前比较http的web服务器的性能. 发现nginx 比 tomcat 要好 50% 然后想到, https的情况下不知道两者有什么区别 所以准备再尝试一下. 换用https进行检查. Springboot的设置 server: ssl:

【转帖】nginx变量使用方法详解-5

https://www.diewufeiyang.com/post/579.html 前面在 (二) 中我们已经了解到变量值容器的生命期是与请求绑定的,但是我当时有意避开了“请求”的正式定义。大家应当一直默认这里的“请求”都是指客户端发起的 HTTP 请求。其实在 Nginx 世界里有两种类型的“请

这些负载均衡都解决哪些问题?服务、网关、NGINX

这篇文章解答一下群友的一系列提问: 在微服务项目中,有服务的负载均衡、网关的负载均衡、Nginx的负载均衡,这几个负载均衡分别用来解决什么问题呢? 在微服务项目中,服务的负载均衡、网关的负载均衡和Nginx的负载均衡都用于解决不同的问题: 1. 服务的负载均衡: 先抛出一个问题: 当一个微服务被多个

精选版:用Java扩展Nginx(nginx-clojure 入门)

让 Java 代码直接在 Nginx 上运行?这么有趣的功能,随本文一起来实战体验吧,图文并茂,一定能成功的那种实战