当nginx遇上http的delete请求+IIS部署

一个团队闭关开发独立的cs构架的项目,千辛万苦出来了第一个版本,测试通过,线上单台服务器灰度测试通过,很开心,
于是走流程正式发布,按常规项目部署分布式多服务器,结果问题来了,很多访问几乎都报400错误……
整个团队都方了,周末排查,最终定位到如下特点:
- GET/POST/PUT请求都正常,只有DELETE请求必现400错误;
- 单独Host到后端服务器是正常的,只有接入前端nginx的反向代理才会出错
- 同样的请求,用PostMan或Fiddler的Composer发给nginx,却返回正常???

找OP协助排查,OP排查结果:
- nginx的access_log里根本没有DELETE请求日志,IIS上也没有收到nginx转发的DELETE请求
- 其它GET/POST之类的请求,nginx和IIS里都有正常记录
- nginx反代一直正常工作,并且服务了几十个也有DELETE的项目,没出现过问题

于是,该团队以为是c端的代码有问题,导致delete的请求没发出来,或者是超时了,就反复打包发版本测试,搞不定……

最后问题返回到我的手上,因为是C#项目,而且线上问题必现,第一步当然是打日志,把WebRequest.Header和WebException.Response.Header打印出来,该项目client的代码大致缩写如下:

public string GetPage(string url, string param, EnumMethod method, Dictionary<string,string> header)
{
  var request = (HttpWebRequest)WebRequest.Create(url);
  request.Method = method.ToString();
  foreach (var pair in headers)
  {
        request.Headers.Add(pair.Key, pair.Value);
  }
  if (!string.IsNullOrEmpty(param))
  {
    byte[] l_data = encoding.GetBytes(param);
    request.ContentLength = l_data.Length;
    using (Stream newStream = request.GetRequestStream())
    {
        newStream.Write(l_data, 0, l_data.Length);
        newStream.Close();
    }
  }
  else
    request.ContentLength = 0;
      HttpWebResponse response;
  try
  {
    response = (HttpWebResponse)request.GetResponse();
  }
  catch (WebException webExp)
  {
        using (var responseErr = (HttpWebResponse)webExp.Response)
        using (var sr = new StreamReader(responseErr.Stream, encoding))
        {
          //  把错误请求和响应的header一起返回打印
          return "请求头信息:\r\n" + request.Headers + "\r\n\r\n响应头信息:\r\n" + responseErr.Headers +
                       "\r\n\r\n响应内容:\r\n" +
                       html;
          return html;
        }
  }

打印出来的错误响应内容,确实是nginx返回的:

<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>Nginx</center>
</body>
</html>

奇怪,为啥nginx的access日志里没有呢?
接着把打印出来的header一模一样放到Fiddler的Composer里测试,确实能收到正常的200响应,
为避免环境问题,还放到我自己机器上测试,也是正常……

为了理清思路,并用自己熟悉的环境验证,我回到自己的电脑,新建一个项目,纯手写了一段代码,一调试,结果测试通过,返回200了!?!

var request = (HttpWebRequest)WebRequest.Create("http://xxx");
request.Method = "DELETE";
request.Headers.Add("", ""); // 添加刚才那些header
var response = (HttpWebResponse)request.GetResponse();

把上述代码发给团队,替换掉旧的代码,验证通过!!!
于是一行一行的恢复旧代码进行验证,最终定位,问题出在:equest.Method = method.ToString();
这里传入的是“Delete”, 必须要传递完整的大写,而我手写代码,习惯写成大写,一开始也没有注意到该项目是小写!!!
让对方改成DELETE后,问题解决

等会,哪里不太对!!!
看了一下对方项目里的EnumMethod枚举定义:

public enum EnumMethod
{
  Get,
  Post,
  Put,
  Delete
}

另外3个GET/POST/PUT也是小写啊!!
反编译看了一下WebRequest的源代码,在执行:webrequest.Method = “POST”; 时,会调用代码:

return KnownHttpVerb.NamedHeaders[(object) name] as KnownHttpVerb ?? 
    new KnownHttpVerb(name, false, false, false, false);

而KnownHttpVerb的定义如下,它会自动转换GET/POST/PUT为大写,里面居然没有Delete……什么鬼

又看了一下nginx的代码:https://github.com/nginx/nginx/blob/master/src/http/ngx_http_parse.c
里面明确限制了,只支持Method为大写,出错了直接返回:

扫描二维码关注公众号,回复: 1018052 查看本文章
if ((ch < 'A' || ch > 'Z') && ch != '_' && ch != '-') {
    return NGX_HTTP_PARSE_INVALID_METHOD;
}

总结一下问题原因:
- 团队成员不熟悉HTTP协议,代码使用了小写的Method发http请求
- .Net底层会自动转换Get/Post/Put为大写后再发请求,但是遗漏了Delete
- nginx严格遵循http协议,只接收大写的METHOD;
- nginx发现小写Method,直接返回错误,并跳过了后续的记录日志步骤

排查过程走的弯路:
- IIS没有区分大小写,导致测试和单台上线时未能验证到问题;
- nginx没有记录下具体错误日志,无法排查;
- 团队成员没有查看WebException.Response内容,且nginx无日志,以为代码未发现请求,浪费了大量时间;
- PostMan和Fiddler的Composer的METHOD是下拉框,无法手动输入,因此无法重现;
- C#代码里,request.Headers里不包括请求的METHOD和url,所以也未能第一时间验证

问题的最后,宣传一下RFC2616 HTTP协议标准,第5.1.1节: https://tools.ietf.org/html/rfc2616#section-5.1.1
这里明确注明了,HTTP请求的Method是区分大小写的,
可以在这里搜索一下:case-sensitive,知道哪些内容要区分大小写;
再搜索一下:case-insensitive,知道哪些不区分大小写。
当然,对于一个合格的Web开发者,学习和了解http协议是必备知识。

猜你喜欢

转载自blog.csdn.net/youbl/article/details/80034806