Description
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?