Skip to content

net/http.TimeoutHandler: ResponseWriter is wrapped so that Flush cannot be used. #74021

Closed as duplicate of#69777
@gKits

Description

@gKits

Go version

1.24.2

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/<redacted>/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/<redacted>/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build98952603=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/home/<redacted>/go/pkg/mod'
GONOPROXY='<redacted>'
GONOSUMDB='<redacted>'
GOOS='linux'
GOPATH='/home/<redacted>/go'
GOPRIVATE='<redacted>'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/<redacted>/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.24.2'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

I have implemented a HandlerFunc that handles Server Sent Events using the SSE Header and the Flusher interface. Then I wanted to use the http.TimeoutHandler as a middleware to make sure that the SSE connections timeout after a certain amount of time.

This is an example:

package main

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	hn := http.Handler(http.HandlerFunc(handler))
	mux.Handle("GET /", http.TimeoutHandler(hn, 1*time.Minute, "timed out"))
	http.ListenAndServe(":8080", mux)
}

func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	w.Header().Set("Access-Control-Allow-Origin", "*")

	clientGone := r.Context().Done()

	flush, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "writer is not a flusher", http.StatusInternalServerError)
		return
	}
	t := time.NewTicker(time.Second)
	defer t.Stop()
	for {
		select {
		case <-clientGone:
			fmt.Println("Client disconnected")
			return
		case <-t.C:
			_, err := fmt.Fprintf(w, "data: time: %s\n\n", time.Now().Format(time.UnixDate))
			if err != nil {
				return
			}
			flush.Flush()
		}
	}
}

What did you see happen?

When I make a cURL request against that Endpoint I get this result.

$ curl localhost:8080
writer is not a flusher

What did you expect to see?

I expected the ResponseWriter to be assertable to the Flusher interface and the handler function to enter the loop and send SSE messages every second until it times out after 1 minute and the connection is closed by the timeout middleware.


After some research I read this part in the http.Flusher documentation

The default HTTP/1.x and HTTP/2 ResponseWriter implementations support Flusher, but ResponseWriter wrappers may not. Handlers should always test for this ability at runtime.

Which means that the http.TimeoutHandler must wrap the ResponseWriter in a way that does not implement the Flusher interface. I could confirm this in net/http/server.go:3815. The implementation of the timeoutWriter does not provide the Flush function.

Is this a bug or is the TimeoutHandler not intended to be used with SSE and manual calls to Flush?

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions