From 44b868dae93b8999b0df87b01da140ec075197cd Mon Sep 17 00:00:00 2001 From: hallelx2 Date: Fri, 3 Jul 2026 17:04:33 +0100 Subject: [PATCH] fix(middleware): pass through http.Flusher for SSE streaming The request-logging, metrics, and tracing middlewares each wrap the ResponseWriter to capture status/bytes, but none implemented http.Flusher. Streaming handlers (/v1/answer/treewalk?stream=true, /v1/query/stream) do w.(http.Flusher) and errored with 'response writer does not support http.Flusher'. Add Flush() (delegating to the wrapped writer) + Unwrap() to statusRecorder, metricsRecorder, and tracingRecorder so flushes propagate through the whole chain and SSE streams in real time. --- internal/middleware/logging.go | 12 ++++++++++++ internal/middleware/metrics.go | 11 +++++++++++ internal/middleware/tracing.go | 11 +++++++++++ 3 files changed, 34 insertions(+) diff --git a/internal/middleware/logging.go b/internal/middleware/logging.go index b1d8684..16c66c1 100644 --- a/internal/middleware/logging.go +++ b/internal/middleware/logging.go @@ -28,6 +28,18 @@ func (r *statusRecorder) Write(b []byte) (int, error) { return n, err } +// Flush propagates to the wrapped writer so SSE/streaming handlers can +// flush through this middleware. Without it, w.(http.Flusher) fails and +// streaming endpoints error out. +func (r *statusRecorder) Flush() { + if f, ok := r.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// Unwrap exposes the wrapped writer for http.ResponseController. +func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter } + // AccessLog logs one structured line per HTTP request: method, path, // status, duration, request ID, and principal ID. // diff --git a/internal/middleware/metrics.go b/internal/middleware/metrics.go index dc59f63..46a6400 100644 --- a/internal/middleware/metrics.go +++ b/internal/middleware/metrics.go @@ -56,6 +56,17 @@ func (r *metricsRecorder) Write(b []byte) (int, error) { return n, err } +// Flush propagates to the wrapped writer so SSE/streaming handlers can +// flush through the metrics middleware. +func (r *metricsRecorder) Flush() { + if f, ok := r.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// Unwrap exposes the wrapped writer for http.ResponseController. +func (r *metricsRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter } + // normalizePath strips URL parameters to keep metric cardinality // bounded. Document and section IDs are replaced with a placeholder. func normalizePath(path string) string { diff --git a/internal/middleware/tracing.go b/internal/middleware/tracing.go index e0d0bff..f0005c1 100644 --- a/internal/middleware/tracing.go +++ b/internal/middleware/tracing.go @@ -67,6 +67,17 @@ func (r *tracingRecorder) WriteHeader(code int) { r.ResponseWriter.WriteHeader(code) } +// Flush propagates to the wrapped writer so SSE/streaming handlers can +// flush through the tracing middleware. +func (r *tracingRecorder) Flush() { + if f, ok := r.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// Unwrap exposes the wrapped writer for http.ResponseController. +func (r *tracingRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter } + // TraceIDFromContext extracts the trace ID string from the current span, // useful for correlating logs with traces. func TraceIDFromContext(ctx context.Context) string {