go-zero icon indicating copy to clipboard operation
go-zero copied to clipboard

Failed on streaming response when timeout enabled.

Open midy177 opened this issue 2 years ago • 4 comments

timeouthandler开启时会导致streaming-response失败 1、无法再写入新的header头,handler函数中无法通过w.Header()获取到中间件设置的响应头 解决方法 将rest/handler/timeouthandler.go 71行代码 h: make(http.Header), 改为 h: w.Header(),

2、无法流式写入响应,需要修改Write相关function

midy177 avatar Aug 03 '23 06:08 midy177

无法流式写入响应,需要修改Write相关function 将rest/handler/timeouthandler.go 方法

func (tw *timeoutWriter) Flush() {
	if flusher, ok := tw.w.(http.Flusher); ok {
		flusher.Flush()
	}
}

修改为

func (tw *timeoutWriter) Flush() {
	if flusher, ok := tw.w.(http.Flusher); ok {
		tw.w.Write(tw.wbuf.Bytes())
		flusher.Flush()
	}
}

midy177 avatar Aug 03 '23 06:08 midy177

The code is looking like below, it should meet your requirements. Please check.

// Flush implements the Flusher interface.
func (tw *timeoutWriter) Flush() {
	flusher, ok := tw.w.(http.Flusher)
	if !ok {
		return
	}

	header := tw.w.Header()
	for k, v := range tw.h {
		header[k] = v
	}

	tw.w.Write(tw.wbuf.Bytes())
	tw.wbuf.Reset()
	flusher.Flush()
}

https://github.com/zeromicro/go-zero/blob/a2e703c53eaaccc82673a8a8e499fae5a3749843/rest/handler/timeouthandler.go#L134

kevwan avatar Aug 05 '23 15:08 kevwan

The code is looking like below, it should meet your requirements. Please check.

// Flush implements the Flusher interface.
func (tw *timeoutWriter) Flush() {
	flusher, ok := tw.w.(http.Flusher)
	if !ok {
		return
	}

	header := tw.w.Header()
	for k, v := range tw.h {
		header[k] = v
	}

	tw.w.Write(tw.wbuf.Bytes())
	 tw.wbuf.Reset()
	flusher.Flush()
}

https://github.com/zeromicro/go-zero/blob/a2e703c53eaaccc82673a8a8e499fae5a3749843/rest/handler/timeouthandler.go#L134

This actually doesn't solve my needs. I can tell you more about the problems I found, because

func (h *timeoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get(headerUpgrade) == valueWebsocket {
		h.handler.ServeHTTP(w, r)
		return
	}

	ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)

The timeout context is set in h.dt after a specific time interval, the connection must be disconnected. Even if the server's streaming response is not over, it will be disconnected. In order to solve this problem, I changed the timeout context to a resettable timer, which will trigger the reset action when the Flush method is called. Changing timeouthandler.go alone is not enough, because this is set in the engine.go part, this is the source code

func (ng *engine) withTimeout() internal.StartOption {
        return func(svr *http.Server) {
                timeout := ng.timeout
                if timeout > 0 {
                        // factor 0.8, to avoid clients send longer content-length than the actual content,
                        // without this timeout setting, the server will time out and respond 503 Service Unavailable,
                        // which triggers the circuit breaker.
                        svr.ReadTimeout = 4 * timeout / 5
                        // factor 1.1, to avoid servers don't have enough time to write responses.
                        // setting the factor less than 1.0 may lead clients not receiving the responses.
                        svr.WriteTimeout = 11 * timeout / 10
                }
        }
}

The write timeout is also set here, which will have a higher priority than the timeouthandler. My personal suggestion is that the engine does not enable the write timeout configuration when the timeouthandler middleware is enabled. All the pr codes I submitted have been verified the pr i submitted

midy177 avatar Aug 11 '23 07:08 midy177

This issue is stale because it has been open for 30 days with no activity.

github-actions[bot] avatar Aug 14 '24 01:08 github-actions[bot]

This issue was closed because it has been inactive for 14 days since being marked as stale.

github-actions[bot] avatar Nov 14 '24 02:11 github-actions[bot]