Skip to content

http.TimeoutHandler panics when used with Gin #4460

@paul-freeman

Description

@paul-freeman

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

In response_writer.go:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugFound something you weren't expecting? Report it here!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions