From daeef95ef367e715b79982ac7e2a502333b36f52 Mon Sep 17 00:00:00 2001 From: Ali Date: Mon, 13 Apr 2026 18:56:28 +0500 Subject: [PATCH] promhttp: skip 1xx informational responses in responseWriterDelegator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WriteHeader recorded the first status code it received, so a 100 Continue sent before the actual response caused the delegator to report 100 as the final status and never invoke observeWriteHeader with the real code. net/http applies the same rule — informational (1xx) responses are forwarded to the underlying ResponseWriter for correct wire behaviour (e.g. the Expect: 100-continue handshake) but do not update the recorded status or the wroteHeader flag. Apply the same logic here: if code is in the 1xx range, forward it and return immediately without touching status or wroteHeader. Adds TestResponseWriterDelegatorIgnores1xxStatus covering the bug-report scenario (100 followed by implicit 200 via Write) and explicit WriteHeader(200) after WriteHeader(100). Fixes #1772 Signed-off-by: Ali --- prometheus/promhttp/delegator.go | 11 +++++ prometheus/promhttp/delegator_test.go | 61 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) 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}