前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Golang那些事】为什么请求处理完了,服务端没有返回呢!

【Golang那些事】为什么请求处理完了,服务端没有返回呢!

原创
作者头像
粲然忧生
发布2025-01-19 00:04:21
发布2025-01-19 00:04:21
1630
举报
文章被收录于专栏:工程师的分享工程师的分享

笔者近期遇到了一个十分奇怪的事情,具体来说一个HTTP请求处理完了,但服务端就是不返回,导致客户端超时......虽然最后用二分法找到了问题所在,但这里设计的原理还是挺值得沉淀的

一个不再返回的请求

如下图所示,请求发送到使用Gin框架的Golang的HTTP服务,服务端的业务逻辑代码走完了,一直没有返回数据到请求方,最终导致请求超时

可以看到,通过在服务端打断点,已经到了业务代码的最后一步

第一个反应是不是有defer函数,走查代码发现没有疑似的defer,Pass

第二个反应是不是有一些公共的钩子函数,通过单步调试发现,最后走到了Gin的框架代码,Pass

第三个反应是不是Gin框架的BUG,继续单步调试,发现走到Golang的内部代码,PaSS

关键在于,不论是业务代码、Gin框架、Golang源代码都没有进入到任何异常逻辑,也没有报错

...历史的经验告诉我们,一般来说是请求体出现了问题

按照先少后多的原则,对请求体进行删减

代码语言:txt
复制
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.必竟有的时候,基于安全等考虑,我们是要对发送过来的请求做一些二次封装,如果要修改的话,在哪一层会好一些?

带着这两个问题,笔者开始的代码阅读,

第一个可能的地方——Gin框架

这里首先看看AI能不能找到?

看来Gin对于这样标准的工作都为委托给了Golang的net/http库

第二个可能的地方——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源码

代码语言:go
复制
// 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库的教程

https://gufeijun.com/post/httpframe/4/

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个不再返回的请求
  • 刨根问底一下代码逻辑
    • 第一个可能的地方——Gin框架
    • 第二个可能的地方——Golang的net/http库
    • 首先找到了一个疑似点
    • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档