笔者近期遇到了一个十分奇怪的事情,具体来说一个HTTP请求处理完了,但服务端就是不返回,导致客户端超时......虽然最后用二分法找到了问题所在,但这里设计的原理还是挺值得沉淀的
如下图所示,请求发送到使用Gin框架的Golang的HTTP服务,服务端的业务逻辑代码走完了,一直没有返回数据到请求方,最终导致请求超时
可以看到,通过在服务端打断点,已经到了业务代码的最后一步
第一个反应是不是有defer函数,走查代码发现没有疑似的defer,Pass
第二个反应是不是有一些公共的钩子函数,通过单步调试发现,最后走到了Gin的框架代码,Pass
第三个反应是不是Gin框架的BUG,继续单步调试,发现走到Golang的内部代码,PaSS
关键在于,不论是业务代码、Gin框架、Golang源代码都没有进入到任何异常逻辑,也没有报错
...历史的经验告诉我们,一般来说是请求体出现了问题
按照先少后多的原则,对请求体进行删减
curl -X POST "http://127.0.0.1:80/test/" -H "Content-Length: 504" -H "Content-Type: application/json" -H "Host: 127.0.0.212" -H "User-Agent: Http" -H "X-Forwarded-For: 127.175.66.27" -H "Language: zh-CN" -H "Timestamp: 1732803011" -d {
"stringTest": "demo1",
"StartTime": 1732662000000,
"EndTime": 1732748400000,
"TimeType": "houe",
"numberTest": 0001,
"Action": "helloworld",
"Id": "1234567890"
}
最后发现:
问题出现在Content-Length这里,去掉就好了,那这里为什么有问题?
经对比,发现Context-Length与请求真实长度不一致,由于这个请求是从另一个请求借鉴而来,所以Header里面复用了之前的Content-Length
而如果Content-Length的长度大于body真实的长度,会发生什么呢?
我们问下AI
简单来说,导致服务端认为还有数据没有上报过来,所以一直在等待接收,从而导致超时
这里在请求的时候先去掉Content-Length,使用一些成熟的请求工具(比如jmeter)会自动计算并进行添加,这样就不会出现这个问题了
嗯,似乎问题很简单,但我们刨根问题一下,
1.首先为什么业务代码会执行,而不是等数据都来了再执行?
2.这里是Gin框架导致的,还是Golang导致的,还是更底层导致的呢?
3.必竟有的时候,基于安全等考虑,我们是要对发送过来的请求做一些二次封装,如果要修改的话,在哪一层会好一些?
带着这两个问题,笔者开始的代码阅读,
这里首先看看AI能不能找到?
看来Gin对于这样标准的工作都为委托给了Golang的net/http库
还是先看看AI能不能找到?
AI给出了三个线索文件,去看一下
上图文件在transfer.go文件里面的525行,这里是对length进行了操作,仔细走查发现,其实这里只是为了确认Content-Length值有效性
再然后看下
上图文件在transfer.go文件里面的568行,这里是把读取body的值做一个限制,即用Content-Length进行限制
这里是关键
因为这里基本上确定了要读取的长度,而body比Content-Length小的话,是读不完的
再往下
上图文件在server.go的2009行,上图中是调用业务代码,这里也就解释了为什么业务代码会执行,即问题一
但问题点出现在2015行,看名字就知道这里是要确定request是否要处理完成
上图文件在server.go的1661行,经测试正常的请求会快速跳过1661行代码,但body比Content-Length小的话,会一直停在这里
看上面的两个图就会知道,基本上在网络IO那里等待接收数据,这里代码已经走到底层网络,不是HTTP了,那啥时候读取body的呢,也就是transfer.go文件里面的568行的reader的内容,因为这里的length设置的不对,答案在下图
比较意外的是并不是在writeBody方法里面,而是在writeHeader方法里面,因为这里需要对部分body进行丢弃,就提前读取了body,从而一直没有读取完毕
1.首先为什么业务代码会执行,而不是等数据都来了再执行?
因为先执行server.go的2009行,在执行2015行,也就是说,在Gin框架里面,先执行业务代码,再判断request是否接收完毕,这里不知道在极端场景是否有问题
2.这里是Gin框架导致的,还是Golang导致的,还是更底层导致的呢?
这里Gin只是推波助澜了一下,本质上还是读取的body没有到达Content-Length,导致网络IO一直在等待
3.必竟有的时候,基于安全等考虑,我们是要对发送过来的请求做一些二次封装,如果要修改的话,在哪一层会好一些?
如果要做二次封装,在Gin框架这里可以在业务代码进行修改body和对应的Content-Length,再进行一次重定向发送即可
收工
另附一个小知识,也不是所有的请求都需要加Content-Length。如果使用的是分块传输编码(Chunked transfer encoding)则添加Transfer-Encoding,即可,
值得注意的是:Transfer-Encoding 和 Content-Length 是互斥的,如果同时出现,浏览器以 Transfer-Encoding 为准,在Go中,会从header中删除两个信息,然后设置TransferEncoding和ContentLength属性
具体可以参考net/http源码
// srs/net/http/transfer.go
// parseTransferEncoding sets t.Chunked based on the Transfer-Encoding header.
func (t *transferReader) parseTransferEncoding() error {
raw, present := t.Header["Transfer-Encoding"]
if !present {
return nil
}
delete(t.Header, "Transfer-Encoding")
// Issue 12785; ignore Transfer-Encoding on HTTP/1.0 requests.
if !t.protoAtLeast(1, 1) {
return nil
}
// Like nginx, we only support a single Transfer-Encoding header field, and
// only if set to "chunked". This is one of the most security sensitive
// surfaces in HTTP/1.1 due to the risk of request smuggling, so we keep it
// strict and simple.
if len(raw) != 1 {
return &unsupportedTEError{fmt.Sprintf("too many transfer encodings: %q", raw)}
}
if !ascii.EqualFold(textproto.TrimString(raw[0]), "chunked") {
return &unsupportedTEError{fmt.Sprintf("unsupported transfer encoding: %q", raw[0])}
}
// RFC 7230 3.3.2 says "A sender MUST NOT send a Content-Length header field
// in any message that contains a Transfer-Encoding header field."
//
// but also: "If a message is received with both a Transfer-Encoding and a
// Content-Length header field, the Transfer-Encoding overrides the
// Content-Length. Such a message might indicate an attempt to perform
// request smuggling (Section 9.5) or response splitting (Section 9.4) and
// ought to be handled as an error. A sender MUST remove the received
// Content-Length field prior to forwarding such a message downstream."
//
// Reportedly, these appear in the wild.
delete(t.Header, "Content-Length")
t.Chunked = true
return nil
}
...
// Unify output
switch rr := msg.(type) {
case *Request:
rr.Body = t.Body
rr.ContentLength = t.ContentLength
if t.Chunked {
rr.TransferEncoding = []string{"chunked"}
}
rr.Close = t.Close
rr.Trailer = t.Trailer
case *Response:
rr.Body = t.Body
rr.ContentLength = t.ContentLength
if t.Chunked {
rr.TransferEncoding = []string{"chunked"}
}
rr.Close = t.Close
rr.Trailer = t.Trail
参考链接:https://loesspie.com/2021/12/17/http-content-length/
另附一个从零写net/http库的教程
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。