Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions prometheus/promhttp/delegator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions prometheus/promhttp/delegator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading