From 46da485481517ad5edb2dcf97c3590354cf62c8e Mon Sep 17 00:00:00 2001 From: Mike Ma Date: Thu, 7 May 2026 08:51:37 -0500 Subject: [PATCH] Avoid panics for unsupported optional response interfaces Wrapped response writers such as http.TimeoutHandler may not implement Hijacker or CloseNotifier. Match the safe optional-interface checks used by Flush and Pusher before calling through. Tested: docker run --rm -v "$PWD":/src -w /src golang:1.25 go test -run 'TestResponseWriterHijack|TestResponseWriterHijackAfterWrite|TestResponseWriterHijackAfterWriteHeaderNow' ./... Tested: docker run --rm -v "$PWD":/src -w /src golang:1.25 go test ./... --- response_writer.go | 11 +++++++++-- response_writer_test.go | 34 ++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/response_writer.go b/response_writer.go index 9035e6f1..eb8f0e37 100644 --- a/response_writer.go +++ b/response_writer.go @@ -114,15 +114,22 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if w.size > 0 { return nil, nil, errHijackAlreadyWritten } + hijacker, ok := w.ResponseWriter.(http.Hijacker) + if !ok { + return nil, nil, http.ErrNotSupported + } if w.size < 0 { w.size = 0 } - return w.ResponseWriter.(http.Hijacker).Hijack() + return hijacker.Hijack() } // CloseNotify implements the http.CloseNotifier interface. func (w *responseWriter) CloseNotify() <-chan bool { - return w.ResponseWriter.(http.CloseNotifier).CloseNotify() + if notifier, ok := w.ResponseWriter.(http.CloseNotifier); ok { + return notifier.CloseNotify() + } + return nil } // Flush implements the http.Flusher interface. diff --git a/response_writer_test.go b/response_writer_test.go index dfc1d2c6..c31cbe76 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -113,19 +114,36 @@ func TestResponseWriterHijack(t *testing.T) { writer.reset(testWriter) w := ResponseWriter(writer) - assert.Panics(t, func() { - _, _, err := w.Hijack() - require.NoError(t, err) - }) - assert.True(t, w.Written()) + _, _, err := w.Hijack() + require.ErrorIs(t, err, http.ErrNotSupported) + assert.False(t, w.Written()) - assert.Panics(t, func() { - w.CloseNotify() - }) + assert.Nil(t, w.CloseNotify()) w.Flush() } +func TestResponseWriterHijackWithTimeoutHandler(t *testing.T) { + var hijackErr error + var closeNotify <-chan bool + var written bool + + handler := http.TimeoutHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writer := &responseWriter{} + writer.reset(w) + + _, _, hijackErr = writer.Hijack() + closeNotify = writer.CloseNotify() + written = writer.Written() + }), time.Second, "") + + handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/", nil)) + + require.ErrorIs(t, hijackErr, http.ErrNotSupported) + assert.Nil(t, closeNotify) + assert.False(t, written) +} + type mockHijacker struct { *httptest.ResponseRecorder hijacked bool