Files
Hudson CavazinandGitHub ccf1b61e20 httpp: propagate Flush() through the response writer wrappers (#6126)
handlerLogger's responseRecorder and handlerWriteTimeout's writeTimeoutWriter
both wrap http.ResponseWriter but implement neither http.Flusher nor Unwrap().
Any handler that flushes mid-response has that flush silently dropped, and its
output only reaches the client when the handler returns.

That defeats the stated purpose of writeTimeoutWriter, whose own comment says it
exists so one can "write long responses, splitted in chunks, without causing
timeouts": the deadline is reset per Write(), but nothing ever leaves the buffer
until the end.

Found while adding a server-sent-events endpoint: events written and flushed at
t=0.4s only reached the client at t=2.2s, when the handler returned.

Adding Flush() plus Unwrap() (so http.ResponseController can reach through) fixes
both. The new test drives a real listener through both wrappers and reads while
the handler is still blocked, so it fails on the current code by hitting the read
deadline.
2026-08-24 14:40:43 +00:00

59 lines
1.4 KiB
Go

package httpp
import (
"net/http"
"time"
)
type writeTimeoutWriter struct {
w http.ResponseWriter
rc *http.ResponseController
timeout time.Duration
}
func (w *writeTimeoutWriter) Header() http.Header {
return w.w.Header()
}
func (w *writeTimeoutWriter) Write(p []byte) (int, error) {
w.rc.SetWriteDeadline(time.Now().Add(w.timeout)) //nolint:errcheck
return w.w.Write(p)
}
func (w *writeTimeoutWriter) WriteHeader(statusCode int) {
w.rc.SetWriteDeadline(time.Now().Add(w.timeout)) //nolint:errcheck
w.w.WriteHeader(statusCode)
}
// apply write deadline before every Write() call.
// this allows to write long responses, splitted in chunks,
// without causing timeouts.
type handlerWriteTimeout struct {
h http.Handler
timeout time.Duration
}
func (h *handlerWriteTimeout) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ww := &writeTimeoutWriter{
w: w,
rc: http.NewResponseController(w),
timeout: h.timeout,
}
h.h.ServeHTTP(ww, r)
}
// Flush propagates the flush to the wrapped writer. Without it, a handler that
// streams a long response (server-sent events) has its output held until it
// returns, which is exactly the case the type was written to support.
func (w *writeTimeoutWriter) Flush() {
if f, ok := w.w.(http.Flusher); ok {
f.Flush()
}
}
// Unwrap lets http.ResponseController reach the underlying writer.
func (w *writeTimeoutWriter) Unwrap() http.ResponseWriter {
return w.w
}