写个网关还是很能练手的,这不在对接新的项目时,就遇到题头的报警。

_, err = fmt.Fprintf(w, string(str))
if err != nil {
	fmt.Println(err.Error())
}

一开始,没想到这块会报错,而是直接忽略的,这样就造成了测试时啥也没返回,但是上游是有数据返回的,最后追踪到这里。

于是,便开始万能的断点调试了,发现 w 也就是 http.ResponseWriter 有两个属性是对应此错误的,分别是 written 和 contentLength。其中 contentLength 是接口返回值中 body 的长度,在初始化时值为 -1。

// net/http/server.go

// Read next request from connection.
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
	……
	w = &response{
		conn:          c,
		cancelCtx:     cancelCtx,
		req:           req,
		reqBody:       req.Body,
		handlerHeader: make(Header),
		contentLength: -1,
		closeNotifyCh: make(chan bool, 1),

		// We populate these ahead of time so we're not
		// reading from req.Header after their Handler starts
		// and maybe mutates it (Issue 14940)
		wants10KeepAlive: req.wantsHttp10KeepAlive(),
		wantsClose:       req.wantsClose(),
	}
	……

}

而题头的错误,也定义此文件中。

// net/http/server.go

// ErrContentLength is returned by ResponseWriter.Write calls
// when a Handler set a Content-Length response header with a
// declared size and then attempted to write more bytes than
// declared.
ErrContentLength = errors.New("http: wrote more than the declared Content-Length")

如果返回体的 header 头直接有 Content-Length ,那么便直接读取的此值。

// net/http/server.go

func (w *response) WriteHeader(code int) {
	……
	if cl := w.handlerHeader.get("Content-Length"); cl != "" {
		v, err := strconv.ParseInt(cl, 10, 64)
		if err == nil && v >= 0 {
			w.contentLength = v
		} else {
			w.conn.server.logf("http: invalid Content-Length of %q", cl)
			w.handlerHeader.Del("Content-Length")
		}
	}
	……
}

如果是返回体 header 有 Transfer-Encoding: chunked ,那么 contentLength 便为 -1。

接下来便进入了最后的判断处。

// net/http/server.go

// either dataB or dataS is non-zero.
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {
	……
	w.written += int64(lenData) // ignoring errors, for errorKludge
	if w.contentLength != -1 && w.written > w.contentLength {
		return 0, ErrContentLength
	}
	……
}

这里,如果 contentLength 不是初始值(也就是返回体 header 头 Content-Length 有值),且 written 的值大于 contentLength 的值,便会报 wrote more than the declared Content-Length。

到此,就知道原因和如何解决了。

之所以出现这个问题,是因为,网关接收转发的返回值已经有了 Content-Length 值,而我的网关会在外面包一层,这样就会出现 written 的值大于 contentLength 的 case 了。想要规避,也很简单,让下游返回的头中加上 Transfer-Encoding:chunked ,这样便不会传 Content-Length 的响应头了。于是,在走上述判断的时便因条件不足而跳过了。

最后的最后,上述解决方案还是有有待提高,还是自身网关健壮的好,因此需要考虑目前出现这情况下,不让三方修改代码,而自身兼容考虑好此种情形。

strByte, _ = json.Marshal(ret)
str := string(strByte)
w.Header().Set("Content-Length", strconv.Itoa(len(str)))
w.WriteHeader(http.StatusOK)

之所以这么做是因为,这样可以更新 contentLength 值,当然了,具体更新是第四行代码,也就是熟读代码并掌握实现后如何为己所用了。这里贴出完整的代码。

func (w *response) WriteHeader(code int) {
	if w.conn.hijacked() {
		caller := relevantCaller()
		w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		return
	}
	if w.wroteHeader {
		caller := relevantCaller()
		w.conn.server.logf("http: superfluous response.WriteHeader call from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
		return
	}
	checkWriteHeaderCode(code)
	w.wroteHeader = true
	w.status = code

	if w.calledHeader && w.cw.header == nil {
		w.cw.header = w.handlerHeader.Clone()
	}

	if cl := w.handlerHeader.get("Content-Length"); cl != "" {
		v, err := strconv.ParseInt(cl, 10, 64)
		if err == nil && v >= 0 {
			w.contentLength = v
		} else {
			w.conn.server.logf("http: invalid Content-Length of %q", cl)
			w.handlerHeader.Del("Content-Length")
		}
	}
}
Logo

欢迎加入西安开发者社区!我们致力于为西安地区的开发者提供学习、合作和成长的机会。参与我们的活动,与专家分享最新技术趋势,解决挑战,探索创新。加入我们,共同打造技术社区!

更多推荐