diff --git a/prometheus/promhttp/delegator.go b/prometheus/promhttp/delegator.go index 315eab5f1..a2e8af6d3 100644 --- a/prometheus/promhttp/delegator.go +++ b/prometheus/promhttp/delegator.go @@ -53,6 +53,17 @@ func (r *responseWriterDelegator) Written() int64 { } func (r *responseWriterDelegator) WriteHeader(code int) { + // Informational (1xx) responses are interim and must not be recorded as + // the final status code. net/http itself applies the same rule — see + // https://github.com/golang/go/blob/go1.24.1/src/net/http/server.go#L1212-L1228. + // We still forward them to the underlying ResponseWriter so the client + // receives the correct wire behaviour (e.g. 100 Continue for Expect + // headers), but we do not update wroteHeader or status so that the + // eventual non-informational response is recorded correctly. + if code >= 100 && code <= 199 { + r.ResponseWriter.WriteHeader(code) + return + } if r.observeWriteHeader != nil && !r.wroteHeader { // Only call observeWriteHeader for the 1st time. It's a bug if // WriteHeader is called more than once, but we want to protect diff --git a/prometheus/promhttp/delegator_test.go b/prometheus/promhttp/delegator_test.go index 4576ae7c0..f444fa56e 100644 --- a/prometheus/promhttp/delegator_test.go +++ b/prometheus/promhttp/delegator_test.go @@ -54,6 +54,67 @@ func (rw *responseWriter) SetReadDeadline(deadline time.Time) error { return nil } +// trackingResponseWriter records every WriteHeader call so tests can assert +// which status codes were forwarded to the underlying ResponseWriter. +type trackingResponseWriter struct { + responseWriter + codes []int +} + +func (rw *trackingResponseWriter) WriteHeader(code int) { + rw.codes = append(rw.codes, code) +} + +func TestResponseWriterDelegatorIgnores1xxStatus(t *testing.T) { + t.Run("100 Continue does not become the final status", func(t *testing.T) { + observed := 0 + w := &trackingResponseWriter{} + rwd := &responseWriterDelegator{ + ResponseWriter: w, + observeWriteHeader: func(code int) { + observed = code + }, + } + + // Simulate a handler that sends 100 Continue then writes a body + // (which implicitly triggers a 200 OK). + rwd.WriteHeader(http.StatusContinue) + rwd.Write([]byte("hello")) + + if rwd.Status() != http.StatusOK { + t.Errorf("expected status 200, got %d", rwd.Status()) + } + if observed != http.StatusOK { + t.Errorf("expected observeWriteHeader called with 200, got %d", observed) + } + // 100 must still have been forwarded to the underlying ResponseWriter. + if len(w.codes) < 1 || w.codes[0] != http.StatusContinue { + t.Errorf("expected 100 forwarded to underlying writer, got %v", w.codes) + } + }) + + t.Run("explicit 200 after 100 Continue is recorded correctly", func(t *testing.T) { + observed := 0 + w := &trackingResponseWriter{} + rwd := &responseWriterDelegator{ + ResponseWriter: w, + observeWriteHeader: func(code int) { + observed = code + }, + } + + rwd.WriteHeader(http.StatusContinue) + rwd.WriteHeader(http.StatusOK) + + if rwd.Status() != http.StatusOK { + t.Errorf("expected status 200, got %d", rwd.Status()) + } + if observed != http.StatusOK { + t.Errorf("expected observeWriteHeader called with 200, got %d", observed) + } + }) +} + func TestResponseWriterDelegatorUnwrap(t *testing.T) { w := &responseWriter{} rwd := &responseWriterDelegator{ResponseWriter: w}