-
Notifications
You must be signed in to change notification settings - Fork 8.5k
Description
Description
responseWriter.Flush() panics when underlying ResponseWriter doesn't implement http.Flusher
Description
Gin's responseWriter.Flush() method performs a direct type assertion without checking if the underlying ResponseWriter implements http.Flusher. This causes a panic when Gin is used with middleware that wraps the ResponseWriter with a type that doesn't implement Flusher (e.g., http.TimeoutHandler).
How to reproduce
Expected behavior
The Flush() call should be a no-op when the underlying writer doesn't support flushing, similar to how httputil.ReverseProxy handles it.
Actual behavior
Panic:
interface conversion: *http.timeoutWriter is not http.Flusher: missing method Flush
Stack trace points to response_writer.go:123:
(*responseWriter).Flush: w.ResponseWriter.(http.Flusher).Flush()
Root cause
func (w *responseWriter) Flush() {
w.ResponseWriter.(http.Flusher).Flush()
}This performs an unchecked type assertion that panics if the underlying ResponseWriter doesn't implement http.Flusher.
Suggested fix
func (w *responseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
This is the standard pattern used by httputil.ReverseProxy and other stdlib code.
Gin Version
v1.10.1
Can you reproduce the bug?
Yes
Source Code
package main
import (
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/gin-gonic/gin"
)
func main() {
// Backend that streams data
go func() {
http.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
for i := 0; i < 5; i++ {
w.Write(make([]byte, 50*1024))
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
time.Sleep(100 * time.Millisecond)
}
})
http.ListenAndServe(":8081", nil)
}()
time.Sleep(100 * time.Millisecond)
// Gin proxy wrapped with TimeoutHandler
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
backendURL, _ := url.Parse("http://localhost:8081")
proxy := httputil.NewSingleHostReverseProxy(backendURL)
proxy.FlushInterval = 100 * time.Millisecond
r.GET("/download", func(c *gin.Context) {
proxy.ServeHTTP(c.Writer, c.Request)
})
// http.TimeoutHandler wraps ResponseWriter with *http.timeoutWriter
// which does NOT implement http.Flusher
handler := http.TimeoutHandler(r, 30*time.Second, "timeout")
http.ListenAndServe(":8080", handler)
}Run the server and then:
curl http://localhost:8080/download -o /dev/null
Go Version
v1.25.5
Operating System
macos