[golang] gin的中間中調用Abort方法導致的帶附件的表單提交時,瀏覽器報net::ERR_CONNECTION_RESET錯誤的原因及解決方法

一、軟件環境

服務端基於[email protected][email protected] 開發

瀏覽器端使用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?

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章