Skip to content

Commit 7ba246a

Browse files
authored
Ensure remote write v2 headers cannot be returned on v1 requests (#1927)
Signed-off-by: Kyle Eckhart <[email protected]>
1 parent 8d46898 commit 7ba246a

File tree

4 files changed

+73
-10
lines changed

4 files changed

+73
-10
lines changed

exp/api/remote/remote_api.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -513,8 +513,8 @@ func SnappyDecodeMiddleware(logger *slog.Logger) func(http.Handler) http.Handler
513513
}
514514
}
515515

516-
// NewWriteHandler returns HTTP handler that receives Remote Write 2.0
517-
// protocol https://prometheus.io/docs/specs/remote_write_spec_2_0/.
516+
// NewWriteHandler returns an HTTP handler that can receive the Remote Write 1.0 or Remote Write 2.0
517+
// (https://prometheus.io/docs/specs/remote_write_spec_2_0/) protocol.
518518
func NewWriteHandler(store writeStorage, acceptedMessageTypes MessageTypes, opts ...WriteHandlerOption) http.Handler {
519519
o := writeHandlerOpts{
520520
logger: slog.New(nopSlogHandler{}),
@@ -603,8 +603,8 @@ func (h *writeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
603603
writeResponse = NewWriteResponse()
604604
}
605605

606-
// Set required X-Prometheus-Remote-Write-Written-* response headers, in all cases, along with any user-defined headers.
607-
writeResponse.writeHeaders(w)
606+
// Set any necessary response headers.
607+
writeResponse.writeHeaders(msgType, w)
608608

609609
if storeErr != nil {
610610
if writeResponse.statusCode == 0 {

exp/api/remote/remote_api_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,44 @@ func TestRetryAfterDuration(t *testing.T) {
6565
}
6666
}
6767

68+
type defaultResponseStore struct{}
69+
70+
func (m *defaultResponseStore) Store(*http.Request, WriteMessageType) (*WriteResponse, error) {
71+
return NewWriteResponse(), nil
72+
}
73+
74+
func Test_WriteHandler_V1HandlingDoesNotAddV2Headers(t *testing.T) {
75+
tLogger := slog.Default()
76+
77+
h := NewWriteHandler(&defaultResponseStore{}, MessageTypes{WriteV2MessageType, WriteV1MessageType}, WithWriteHandlerLogger(tLogger))
78+
79+
body := "test"
80+
bodyBytes := snappy.Encode(nil, []byte(body))
81+
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(bodyBytes))
82+
req.Header.Set("Content-Type", "application/x-protobuf")
83+
rec := httptest.NewRecorder()
84+
85+
h.ServeHTTP(rec, req)
86+
87+
if rec.Code != http.StatusNoContent {
88+
t.Fatalf("expected status code 204, got %d with body %s", rec.Code, rec.Body.String())
89+
}
90+
91+
samplesHeader := rec.Header().Get(writtenSamplesHeader)
92+
exemplarsHeader := rec.Header().Get(writtenExemplarsHeader)
93+
histogramHeader := rec.Header().Get(writtenHistogramsHeader)
94+
95+
if samplesHeader != "" {
96+
t.Fatal("expected no written samples header, got", samplesHeader)
97+
}
98+
if exemplarsHeader != "" {
99+
t.Fatal("expected no written exemplars header, got", exemplarsHeader)
100+
}
101+
if histogramHeader != "" {
102+
t.Fatal("expected no written histograms header, got", histogramHeader)
103+
}
104+
}
105+
68106
type mockStorage struct {
69107
v2Reqs []*writev2.Request
70108
protos []WriteMessageType

exp/api/remote/remote_headers.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,18 @@ func (w *WriteResponse) SetExtraHeader(key, value string) {
132132

133133
// writeHeaders sets response headers in a given response writer.
134134
// Make sure to use it before http.ResponseWriter.WriteHeader and .Write.
135-
func (w *WriteResponse) writeHeaders(rw http.ResponseWriter) {
135+
func (w *WriteResponse) writeHeaders(msgType WriteMessageType, rw http.ResponseWriter) {
136136
h := rw.Header()
137-
h.Set(writtenSamplesHeader, strconv.Itoa(w.Samples))
138-
h.Set(writtenHistogramsHeader, strconv.Itoa(w.Histograms))
139-
h.Set(writtenExemplarsHeader, strconv.Itoa(w.Exemplars))
137+
138+
// TODO make it easier to indicate if the stats are valid before adding the headers. WriteResponseStats.confirmed
139+
// could be used if there was a reliable way for it to be set without parsing headers. For now ensure we don't
140+
// add stats headers for v1 messages which can cause confusion/false positive errors logs.
141+
if msgType != WriteV1MessageType {
142+
h.Set(writtenSamplesHeader, strconv.Itoa(w.Samples))
143+
h.Set(writtenHistogramsHeader, strconv.Itoa(w.Histograms))
144+
h.Set(writtenExemplarsHeader, strconv.Itoa(w.Exemplars))
145+
}
146+
140147
for k, v := range w.extraHeaders {
141148
for _, vv := range v {
142149
h.Add(k, vv)

exp/api/remote/remote_headers_test.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,25 @@ func TestWriteResponse(t *testing.T) {
7171
}
7272
})
7373

74-
t.Run("writeHeaders", func(t *testing.T) {
74+
t.Run("writeHeaders v1", func(t *testing.T) {
75+
resp := NewWriteResponse()
76+
resp.SetExtraHeader("Custom-Header", "custom-value")
77+
78+
w := httptest.NewRecorder()
79+
resp.writeHeaders(WriteV1MessageType, w)
80+
81+
expectedHeaders := map[string]string{
82+
"Custom-Header": "custom-value",
83+
}
84+
85+
for k, want := range expectedHeaders {
86+
if got := w.Header().Get(k); got != want {
87+
t.Errorf("header %q: want %q, got %q", k, want, got)
88+
}
89+
}
90+
})
91+
92+
t.Run("writeHeaders v2", func(t *testing.T) {
7593
resp := NewWriteResponse()
7694
resp.Add(WriteResponseStats{
7795
Samples: 10,
@@ -82,7 +100,7 @@ func TestWriteResponse(t *testing.T) {
82100
resp.SetExtraHeader("Custom-Header", "custom-value")
83101

84102
w := httptest.NewRecorder()
85-
resp.writeHeaders(w)
103+
resp.writeHeaders(WriteV2MessageType, w)
86104

87105
expectedHeaders := map[string]string{
88106
"Custom-Header": "custom-value",

0 commit comments

Comments
 (0)