[golang] gin的中间中调用Abort方法导致的带附件的表单提交时,浏览器报net::ERR_CONNECTION_RESET错误的原因及解决方法
一、软件环境
服务端基于go@1.19.3, gin@1.8.2 开发
浏览器端使用chrome、edge及firefox进行测试
二、错误再现
server端使用gin中间件技术自定义一些中间件,这里导致问题出现的主要涉及到“授权监测”和“附件大小限制”两个中间件。之所以如此是因为这两个中间件中都有需要调用gin context的Abort方法的情况。
这里就以很简单的限制附件大小的中间件为例,其基本代码如下:
func SizeLimit(mb int) gin.HandlerFunc { return func(c *gin.Context) { slog.Warn("[SizeLimit] middleware invoked") if c.Request.Method == "GET" || c.Request.Method == "DELETE" {
return } ct := c.Request.Header.Get("Content-Type") if !strings.HasPrefix(ct, "multipart/form-data; boundary") { return } if mb > 0 { bytes := mb * 1024 * 1024 maxBodyBytes := bytes + 512 sl := c.Request.Header.Get("Content-Length") slog.Warn("[SizeLimit]", "c.Request.Header.Content-Length", sl) if len(sl) > 0 { cl, _ := strconv.Atoi(sl) if cl > maxBodyBytes { msg := fmt.Sprintf("附件过大,超出限制。(size limit: %dMb)", mb) slog.Error(msg, errors.New(msg)) if clientAcceptJson(c) { c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{ "code": http.StatusRequestEntityTooLarge, "data": EmptyDs, "msg": msg, }) return } else { c.Abort() c.String(http.StatusRequestEntityTooLarge, "S:"+msg) return } } } else { var w http.ResponseWriter = c.Writer c.Request.Body = http.MaxBytesReader(w, c.Request.Body, int64(maxBodyBytes)) } } } }
engine.Use(SizeLimit(2))
客户端使用chrome访问,编辑一条数据并添加超过限制大小的图片,此时用提交数据就会报那个经典的错误net::ERR_CONNECTION_RESET(当然,只能在浏览器console终端中看到)
Edge也报出同样错误,Firefox则不会在console中打出来,但也是可以catch到网络失败错误(我用的jquery jqXHR.status ==0)。
此时,查看server端log输出则一切正常
time=2023-02-09T14:44:42.053+08:00 level=WARN source=D:/GolangDev/projects/sps/middlewares.go:126 msg="[SizeLimit] middleware invoked" time=2023-02-09T14:44:42.053+08:00 level=WARN source=D:/GolangDev/projects/sps/middlewares.go:140 msg=[SizeLimit] c.Request.Header.Content-Length=9792577 time=2023-02-09T14:44:42.053+08:00 level=ERROR source=D:/GolangDev/projects/sps/middlewares.go:145 msg="附件过大,超出限制。(size limit: 5Mb)" err="附件过大,超出限制。(size limit: 5Mb)" [GIN] 2023/02/09 - 14:44:42 | 413 | 0s | 192.168.2.26 | PUT "/admin/dicts/customers/1?r_num=617123760302072" response size:80
使用ApiFox程序测试接口,一切正常,可以收到server返回的413错误及JSON信息。
遂利用ApiFox生成的测试代码,分别测试了 shell脚本、python、ruby、javascript及go代码,其中javascript脚本和页面代码中一致不能正确收到server响应,其他语言除了ruby则都可以正常返回。ruby则表现的和javascript脚本一样是网络错误。
三、原因排查
1,提交数据但是不带附件,或附件大小不超标,则一切正常(没有被拦截);
2,但是如果我的自定义大小设置很小,比如1Mb,上传1.7mb的文件,虽然被拦截了,但是可以正常收到响应!(迷惑)
3,改为POST方式,同样问题,排除PUT方式问题;
4,bing搜索,看到这篇文章,“Why might a 413 not be flushed to the client immediately?”。
至此,我灵光一现,大概知道了怎么回事儿!上传大的文件时,gin框架没有全部读取完Request数据,此时被中间件拦截并Abort处理后,尽管在中间件中返回了信息,但是浏览器却没有读取,而是始终等着数据发送完成。而此时连接被gin服务端关闭,因此就报出连接reset错误!
小的文件正常是因为gin已经读取解析完成了Reqeust数据(我实验好像2mb以下都没问题),所以浏览器正常接收返回信息了。
四、不是解决方案的解决方案。
虽然不情愿,但是我只能采取读完Request数据并丢弃的方式解决问题,因为程序只能通过浏览器访问,否则用户只能得知发生错误了,不能知道什么错误原因!
我的实现方式是再实现一个中间件,统一处理Abort的Request数据问题。
func RequestTracer() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() req := c.Request slog.Info("[RequestTracer] <---开始--->", "method", req.Method, "request path", req.URL.Path, "client", c.Request.RemoteAddr) // c.Next() // latency := time.Since(t) status := c.Writer.Status() // if c.IsAborted(){ // 读取并丢弃全部Request数据,以免浏览器报错: net::ERR_CONNECTION_RESET buf := make([]byte, 1024) for n, err := c.Request.Body.Read(buf); err == nil && n > 0; n, err = c.Request.Body.Read(buf) { //discard } } slog.Info("[RequestTracer] <---结束--->", "elapased time", latency, "response status", status) } }
五、其他
当然,更进一步解决方式可以在浏览器端实现上传文件大小限制,但是并不能替代服务端的限制,并且考虑到其他中间件也会Abort请求,更是需要服务端处理此问题。
不知道这是否算是浏览器设计中的一个Bug?