Skip to content

Commit 586178b

Browse files
committed
Fix promhttp error handling
Essentially, just don't try to set a status code and send any error message in the body once metrics gathering has succeeded. At that point, the most likely reason for errors is anyway that the client has disconnected, in which sending an error is moot. The other possible reason for an error is a problem during metrics encoding. This is unlikely to happen (it's a coding error here in client_golang in any case), and if it is happening, the odds are we have already sent something to the ResponseWriter, which means we cannot set a status code anymore. The doc comment for HTTPErrorOnError now describes these circumstances explicitly and recommends to set a logger to report that kind of error. This should fix the reason for the infamous `superfluous response.WriteHeader call` message. Signed-off-by: beorn7 <[email protected]>
1 parent 346356f commit 586178b

File tree

2 files changed

+20
-17
lines changed

2 files changed

+20
-17
lines changed

prometheus/promhttp/delegator.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@ func (r *responseWriterDelegator) Written() int64 {
5353
}
5454

5555
func (r *responseWriterDelegator) WriteHeader(code int) {
56+
if r.observeWriteHeader != nil && !r.wroteHeader {
57+
// Only call observeWriteHeader for the 1st time. It's a bug if
58+
// WriteHeader is called more than once, but we want to protect
59+
// against it here. Note that we still delegate the WriteHeader
60+
// to the original ResponseWriter to not mask the bug from it.
61+
r.observeWriteHeader(code)
62+
}
5663
r.status = code
5764
r.wroteHeader = true
5865
r.ResponseWriter.WriteHeader(code)
59-
if r.observeWriteHeader != nil {
60-
r.observeWriteHeader(code)
61-
}
6266
}
6367

6468
func (r *responseWriterDelegator) Write(b []byte) (int, error) {

prometheus/promhttp/http.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,12 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
167167

168168
enc := expfmt.NewEncoder(w, contentType)
169169

170-
var lastErr error
171-
172170
// handleError handles the error according to opts.ErrorHandling
173171
// and returns true if we have to abort after the handling.
174172
handleError := func(err error) bool {
175173
if err == nil {
176174
return false
177175
}
178-
lastErr = err
179176
if opts.ErrorLog != nil {
180177
opts.ErrorLog.Println("error encoding and sending metric family:", err)
181178
}
@@ -184,7 +181,10 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
184181
case PanicOnError:
185182
panic(err)
186183
case HTTPErrorOnError:
187-
httpError(rsp, err)
184+
// We cannot really send an HTTP error at this
185+
// point because we most likely have written
186+
// something to rsp already. But at least we can
187+
// stop sending.
188188
return true
189189
}
190190
// Do nothing in all other cases, including ContinueOnError.
@@ -202,10 +202,6 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
202202
return
203203
}
204204
}
205-
206-
if lastErr != nil {
207-
httpError(rsp, lastErr)
208-
}
209205
})
210206

211207
if opts.Timeout <= 0 {
@@ -276,7 +272,12 @@ type HandlerErrorHandling int
276272
// errors are encountered.
277273
const (
278274
// Serve an HTTP status code 500 upon the first error
279-
// encountered. Report the error message in the body.
275+
// encountered. Report the error message in the body. Note that HTTP
276+
// errors cannot be served anymore once the beginning of a regular
277+
// payload has been sent. Thus, in the (unlikely) case that encoding the
278+
// payload into the negotiated wire format fails, serving the response
279+
// will simply be aborted. Set an ErrorLog in HandlerOpts to detect
280+
// those errors.
280281
HTTPErrorOnError HandlerErrorHandling = iota
281282
// Ignore errors and try to serve as many metrics as possible. However,
282283
// if no metrics can be served, serve an HTTP status code 500 and the
@@ -365,11 +366,9 @@ func gzipAccepted(header http.Header) bool {
365366
}
366367

367368
// httpError removes any content-encoding header and then calls http.Error with
368-
// the provided error and http.StatusInternalServerErrer. Error contents is
369-
// supposed to be uncompressed plain text. However, same as with a plain
370-
// http.Error, any header settings will be void if the header has already been
371-
// sent. The error message will still be written to the writer, but it will
372-
// probably be of limited use.
369+
// the provided error and http.StatusInternalServerError. Error contents is
370+
// supposed to be uncompressed plain text. Same as with a plain http.Error, this
371+
// must not be called if the header or any payload has already been sent.
373372
func httpError(rsp http.ResponseWriter, err error) {
374373
rsp.Header().Del(contentEncodingHeader)
375374
http.Error(

0 commit comments

Comments
 (0)