Implement Server-Sent Events (SSE) in net/http. How do http.ResponseController, flushing and WriteTimeout interact?
SSE is a long-lived text/event-stream response where the server writes data: ...\n\n frames and flushes each one. Two things usually break it: (1) a middleware wrapper hides http.Flusher, and (2) the server's WriteTimeout kills the stream after N seconds. http.ResponseController (Go 1.20) solves both: it walks Unwrap() chains to find a writer that supports Flush, SetWriteDeadline, SetReadDeadline, Hijack and EnableFullDuplex (Go 1.21), returning http.ErrNotSupported otherwise.
func events(w http.ResponseWriter, r *http.Request) {
rc := http.NewResponseController(w)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
t := time.NewTicker(time.Second)
defer t.Stop()
for id := 0; ; id++ {
// Extend the server's WriteTimeout for this long-lived response.
if err := rc.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
select {
case <-r.Context().Done():
return // client disconnected
case now := <-t.C:
fmt.Fprintf(w, "id: %d\ndata: %s\n\n", id, now.Format(time.RFC3339))
if err := rc.Flush(); err != nil {
return // http.ErrNotSupported if no writer in the chain can flush
}
}
}
}
Talking points: a rolling deadline (reset per event) still detects dead clients; http.TimeoutHandler and gzip middleware buffer output and break streaming; reverse proxies (nginx) need buffering disabled (X-Accel-Buffering: no); each open stream holds a goroutine and FD, so cap concurrent subscribers; on HTTP/1.1 browsers limit ~6 connections per origin, HTTP/2 multiplexes them.
More on Standard Library, HTTP & Systems Design in Go
- Q472Fan out N HTTP calls with bounded concurrency, fail fast on first error, and collect results in order.
- Q473How do you unit-test HTTP handlers and HTTP clients with net/http/httptest?
- Q475Write an HTTP client retry helper with exponential backoff and jitter. What makes a request safe to retry?
- Q476Build a reverse proxy with httputil.ReverseProxy. Why is Rewrite preferred over Director?
- Q477How do you test time-dependent concurrent code deterministically? Explain testing/synctest (Go 1.25).