all: consistently return text-based HTTP responses with charset=utf-8

This is a follow-up for https://github.com/VictoriaMetrics/VictoriaMetrics/pull/897
This commit is contained in:
Aliaksandr Valialkin 2020-11-13 10:25:39 +02:00
parent 077f8cbe1c
commit 47a038401b
11 changed files with 40 additions and 38 deletions

View file

@ -228,7 +228,7 @@ func requestHandler(w http.ResponseWriter, r *http.Request) bool {
errMsg := fmt.Sprintf("waiting for scrapes to init, left: %d", rdy) errMsg := fmt.Sprintf("waiting for scrapes to init, left: %d", rdy)
http.Error(w, errMsg, http.StatusTooEarly) http.Error(w, errMsg, http.StatusTooEarly)
} else { } else {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte("OK")) w.Write([]byte("OK"))
} }

View file

@ -82,7 +82,7 @@ func (s *VMStorage) Query(ctx context.Context, query string) ([]Metric, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json; charset=utf-8")
if s.basicAuthPass != "" { if s.basicAuthPass != "" {
req.SetBasicAuth(s.basicAuthUser, s.basicAuthPass) req.SetBasicAuth(s.basicAuthUser, s.basicAuthPass)
} }

View file

@ -28,7 +28,7 @@ func (am *AlertManager) Send(ctx context.Context, alerts []Alert) error {
if err != nil { if err != nil {
return err return err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json; charset=utf-8")
req = req.WithContext(ctx) req = req.WithContext(ctx)
if am.basicAuthPass != "" { if am.basicAuthPass != "" {
req.SetBasicAuth(am.basicAuthUser, am.basicAuthPass) req.SetBasicAuth(am.basicAuthUser, am.basicAuthPass)

View file

@ -40,7 +40,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err) httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
return true return true
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(data) w.Write(data)
return true return true
case "/api/v1/alerts": case "/api/v1/alerts":
@ -49,7 +49,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err) httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
return true return true
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(data) w.Write(data)
return true return true
case "/-/reload": case "/-/reload":
@ -67,7 +67,7 @@ func (rh *requestHandler) handler(w http.ResponseWriter, r *http.Request) bool {
httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err) httpserver.Errorf(w, r, "error in %q: %s", r.URL.Path, err)
return true return true
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write(data) w.Write(data)
return true return true
} }

View file

@ -155,13 +155,13 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
return true return true
case "/targets": case "/targets":
promscrapeTargetsRequests.Inc() promscrapeTargetsRequests.Inc()
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
showOriginalLabels, _ := strconv.ParseBool(r.FormValue("show_original_labels")) showOriginalLabels, _ := strconv.ParseBool(r.FormValue("show_original_labels"))
promscrape.WriteHumanReadableTargetsStatus(w, showOriginalLabels) promscrape.WriteHumanReadableTargetsStatus(w, showOriginalLabels)
return true return true
case "/api/v1/targets": case "/api/v1/targets":
promscrapeAPIV1TargetsRequests.Inc() promscrapeAPIV1TargetsRequests.Inc()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
state := r.FormValue("state") state := r.FormValue("state")
promscrape.WriteAPIV1Targets(w, state) promscrape.WriteAPIV1Targets(w, state)
return true return true
@ -175,7 +175,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
errMsg := fmt.Sprintf("waiting for scrape config to init targets, configs left: %d", rdy) errMsg := fmt.Sprintf("waiting for scrape config to init targets, configs left: %d", rdy)
http.Error(w, errMsg, http.StatusTooEarly) http.Error(w, errMsg, http.StatusTooEarly)
} else { } else {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte("OK")) w.Write([]byte("OK"))
} }

View file

@ -84,9 +84,9 @@ func MetricsFindHandler(startTime time.Time, w http.ResponseWriter, r *http.Requ
} }
paths = deduplicatePaths(paths, delimiter) paths = deduplicatePaths(paths, delimiter)
sortPaths(paths, delimiter) sortPaths(paths, delimiter)
contentType := "application/json" contentType := "application/json; charset=utf-8"
if jsonp != "" { if jsonp != "" {
contentType = "text/javascript" contentType = "text/javascript; charset=utf-8"
} }
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
@ -166,9 +166,9 @@ func MetricsExpandHandler(startTime time.Time, w http.ResponseWriter, r *http.Re
} }
m[query] = paths m[query] = paths
} }
contentType := "application/json" contentType := "application/json; charset=utf-8"
if jsonp != "" { if jsonp != "" {
contentType = "text/javascript" contentType = "text/javascript; charset=utf-8"
} }
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
if groupByExpr { if groupByExpr {
@ -215,9 +215,9 @@ func MetricsIndexHandler(startTime time.Time, w http.ResponseWriter, r *http.Req
if err != nil { if err != nil {
return fmt.Errorf(`cannot obtain metric names: %w`, err) return fmt.Errorf(`cannot obtain metric names: %w`, err)
} }
contentType := "application/json" contentType := "application/json; charset=utf-8"
if jsonp != "" { if jsonp != "" {
contentType = "text/javascript" contentType = "text/javascript; charset=utf-8"
} }
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)

View file

@ -262,19 +262,19 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
case "/api/v1/rules": case "/api/v1/rules":
// Return dumb placeholder // Return dumb placeholder
rulesRequests.Inc() rulesRequests.Inc()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "%s", `{"status":"success","data":{"groups":[]}}`) fmt.Fprintf(w, "%s", `{"status":"success","data":{"groups":[]}}`)
return true return true
case "/api/v1/alerts": case "/api/v1/alerts":
// Return dumb placehloder // Return dumb placehloder
alertsRequests.Inc() alertsRequests.Inc()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "%s", `{"status":"success","data":{"alerts":[]}}`) fmt.Fprintf(w, "%s", `{"status":"success","data":{"alerts":[]}}`)
return true return true
case "/api/v1/metadata": case "/api/v1/metadata":
// Return dumb placeholder // Return dumb placeholder
metadataRequests.Inc() metadataRequests.Inc()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
fmt.Fprintf(w, "%s", `{"status":"success","data":{}}`) fmt.Fprintf(w, "%s", `{"status":"success","data":{}}`)
return true return true
case "/api/v1/admin/tsdb/delete_series": case "/api/v1/admin/tsdb/delete_series":
@ -299,7 +299,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
func sendPrometheusError(w http.ResponseWriter, r *http.Request, err error) { func sendPrometheusError(w http.ResponseWriter, r *http.Request, err error) {
logger.Warnf("error in %q: %s", r.RequestURI, err) logger.Warnf("error in %q: %s", r.RequestURI, err)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
statusCode := http.StatusUnprocessableEntity statusCode := http.StatusUnprocessableEntity
var esc *httpserver.ErrorWithStatusCode var esc *httpserver.ErrorWithStatusCode
if errors.As(err, &esc) { if errors.As(err, &esc) {

View file

@ -88,7 +88,7 @@ func FederateHandler(startTime time.Time, w http.ResponseWriter, r *http.Request
return fmt.Errorf("cannot fetch data for %q: %w", sq, err) return fmt.Errorf("cannot fetch data for %q: %w", sq, err)
} }
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
err = rss.RunParallel(func(rs *netstorage.Result, workerID uint) error { err = rss.RunParallel(func(rs *netstorage.Result, workerID uint) error {
@ -151,7 +151,7 @@ func ExportCSVHandler(startTime time.Time, w http.ResponseWriter, r *http.Reques
MaxTimestamp: end, MaxTimestamp: end,
TagFilterss: tagFilterss, TagFilterss: tagFilterss,
} }
w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Type", "text/csv; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
@ -331,9 +331,9 @@ func exportHandler(w http.ResponseWriter, matches []string, start, end int64, fo
WriteExportJSONLine(bb, xb) WriteExportJSONLine(bb, xb)
resultsCh <- bb resultsCh <- bb
} }
contentType := "application/stream+json" contentType := "application/stream+json; charset=utf-8"
if format == "prometheus" { if format == "prometheus" {
contentType = "text/plain" contentType = "text/plain; charset=utf-8"
writeLineFunc = func(xb *exportBlock, resultsCh chan<- *quicktemplate.ByteBuffer) { writeLineFunc = func(xb *exportBlock, resultsCh chan<- *quicktemplate.ByteBuffer) {
bb := quicktemplate.AcquireByteBuffer() bb := quicktemplate.AcquireByteBuffer()
WriteExportPrometheusLine(bb, xb) WriteExportPrometheusLine(bb, xb)
@ -561,7 +561,7 @@ func LabelValuesHandler(startTime time.Time, labelName string, w http.ResponseWr
} }
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteLabelValuesResponse(bw, labelValues) WriteLabelValuesResponse(bw, labelValues)
@ -639,7 +639,7 @@ func LabelsCountHandler(startTime time.Time, w http.ResponseWriter, r *http.Requ
if err != nil { if err != nil {
return fmt.Errorf(`cannot obtain label entries: %w`, err) return fmt.Errorf(`cannot obtain label entries: %w`, err)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteLabelsCountResponse(bw, labelEntries) WriteLabelsCountResponse(bw, labelEntries)
@ -690,7 +690,7 @@ func TSDBStatusHandler(startTime time.Time, w http.ResponseWriter, r *http.Reque
if err != nil { if err != nil {
return fmt.Errorf(`cannot obtain tsdb status for date=%d, topN=%d: %w`, date, topN, err) return fmt.Errorf(`cannot obtain tsdb status for date=%d, topN=%d: %w`, date, topN, err)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteTSDBStatusResponse(bw, status) WriteTSDBStatusResponse(bw, status)
@ -760,7 +760,7 @@ func LabelsHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
} }
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteLabelsResponse(bw, labels) WriteLabelsResponse(bw, labels)
@ -826,7 +826,7 @@ func SeriesCountHandler(startTime time.Time, w http.ResponseWriter, r *http.Requ
if err != nil { if err != nil {
return fmt.Errorf("cannot obtain series count: %w", err) return fmt.Errorf("cannot obtain series count: %w", err)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteSeriesCountResponse(bw, n) WriteSeriesCountResponse(bw, n)
@ -883,7 +883,7 @@ func SeriesHandler(startTime time.Time, w http.ResponseWriter, r *http.Request)
return fmt.Errorf("cannot fetch data for %q: %w", sq, err) return fmt.Errorf("cannot fetch data for %q: %w", sq, err)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
resultsCh := make(chan *quicktemplate.ByteBuffer) resultsCh := make(chan *quicktemplate.ByteBuffer)
@ -1020,7 +1020,7 @@ func QueryHandler(startTime time.Time, w http.ResponseWriter, r *http.Request) e
} }
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteQueryResponse(bw, result) WriteQueryResponse(bw, result)
@ -1119,7 +1119,7 @@ func queryRangeHandler(startTime time.Time, w http.ResponseWriter, query string,
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/153 // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/153
result = removeEmptyValuesAndTimeseries(result) result = removeEmptyValuesAndTimeseries(result)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
bw := bufferedwriter.Get(w) bw := bufferedwriter.Get(w)
defer bufferedwriter.Put(bw) defer bufferedwriter.Put(bw)
WriteQueryRangeResponse(bw, result) WriteQueryRangeResponse(bw, result)

View file

@ -251,7 +251,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
switch path { switch path {
case "/create": case "/create":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
snapshotPath, err := Storage.CreateSnapshot() snapshotPath, err := Storage.CreateSnapshot()
if err != nil { if err != nil {
err = fmt.Errorf("cannot create snapshot: %w", err) err = fmt.Errorf("cannot create snapshot: %w", err)
@ -265,7 +265,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
} }
return true return true
case "/list": case "/list":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
snapshots, err := Storage.ListSnapshots() snapshots, err := Storage.ListSnapshots()
if err != nil { if err != nil {
err = fmt.Errorf("cannot list snapshots: %w", err) err = fmt.Errorf("cannot list snapshots: %w", err)
@ -282,7 +282,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
fmt.Fprintf(w, `]}`) fmt.Fprintf(w, `]}`)
return true return true
case "/delete": case "/delete":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
snapshotName := r.FormValue("snapshot") snapshotName := r.FormValue("snapshot")
if err := Storage.DeleteSnapshot(snapshotName); err != nil { if err := Storage.DeleteSnapshot(snapshotName); err != nil {
err = fmt.Errorf("cannot delete snapshot %q: %w", snapshotName, err) err = fmt.Errorf("cannot delete snapshot %q: %w", snapshotName, err)
@ -292,7 +292,7 @@ func RequestHandler(w http.ResponseWriter, r *http.Request) bool {
fmt.Fprintf(w, `{"status":"ok"}`) fmt.Fprintf(w, `{"status":"ok"}`)
return true return true
case "/delete_all": case "/delete_all":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
snapshots, err := Storage.ListSnapshots() snapshots, err := Storage.ListSnapshots()
if err != nil { if err != nil {
err = fmt.Errorf("cannot list snapshots: %w", err) err = fmt.Errorf("cannot list snapshots: %w", err)

View file

@ -10,6 +10,8 @@
* FEATURE: add `-search.treatDotsAsIsInRegexps` command-line flag, which can be used for automatic escaping of dots in regexp label filters used in queries. * FEATURE: add `-search.treatDotsAsIsInRegexps` command-line flag, which can be used for automatic escaping of dots in regexp label filters used in queries.
For example, if `-search.treatDotsAsIsInRegexps` is set, then the query `foo{bar=~"aaa.bb.cc|dd.eee"}` is automatically converted to `foo{bar=~"aaa\\.bb\\.cc|dd\\.eee"}`. For example, if `-search.treatDotsAsIsInRegexps` is set, then the query `foo{bar=~"aaa.bb.cc|dd.eee"}` is automatically converted to `foo{bar=~"aaa\\.bb\\.cc|dd\\.eee"}`.
This may be useful for querying Graphite data. This may be useful for querying Graphite data.
* FEATURE: consistently return text-based HTTP responses such as `plain/text` and `application/json` with `charset=utf-8`.
See https://github.com/VictoriaMetrics/VictoriaMetrics/pull/897
* BUGFIX: do not return data points in the end of the selected time range for time series ending in the middle of the selected time range. * BUGFIX: do not return data points in the end of the selected time range for time series ending in the middle of the selected time range.
See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/887 and https://github.com/VictoriaMetrics/VictoriaMetrics/issues/845 See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/887 and https://github.com/VictoriaMetrics/VictoriaMetrics/issues/845

View file

@ -208,7 +208,7 @@ func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh Reques
r.URL.Path = path r.URL.Path = path
switch r.URL.Path { switch r.URL.Path {
case "/health": case "/health":
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
deadline := atomic.LoadInt64(&s.shutdownDelayDeadline) deadline := atomic.LoadInt64(&s.shutdownDelayDeadline)
if deadline <= 0 { if deadline <= 0 {
w.Write([]byte("OK")) w.Write([]byte("OK"))
@ -244,7 +244,7 @@ func handlerWrapper(s *server, w http.ResponseWriter, r *http.Request, rh Reques
return return
} }
startTime := time.Now() startTime := time.Now()
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
WritePrometheusMetrics(w) WritePrometheusMetrics(w)
metricsHandlerDuration.UpdateDuration(startTime) metricsHandlerDuration.UpdateDuration(startTime)
return return
@ -395,7 +395,7 @@ func (zrw *gzipResponseWriter) Write(p []byte) (int, error) {
if h.Get("Content-Type") == "" { if h.Get("Content-Type") == "" {
// Disable auto-detection of content-type, since it // Disable auto-detection of content-type, since it
// is incorrectly detected after the compression. // is incorrectly detected after the compression.
h.Set("Content-Type", "text/html") h.Set("Content-Type", "text/html; charset=utf-8")
} }
} }
zrw.writeHeader() zrw.writeHeader()