From 1ae23bf5e9e4e922e89343d8584fca404b840346 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Tue, 26 Jan 2016 01:32:46 +0100 Subject: [PATCH] Handle OPTIONS HTTP requests correctly. Fixes https://github.com/prometheus/prometheus/issues/1346 --- .../prometheus/common/route/route.go | 5 ++++ vendor/vendor.json | 4 +-- web/api/legacy/api.go | 6 ++++ web/api/legacy/query.go | 8 ++++- web/api/v1/api.go | 26 +++++++++++++---- web/api/v1/api_test.go | 29 +++++++++++++++++++ 6 files changed, 69 insertions(+), 9 deletions(-) diff --git a/vendor/github.com/prometheus/common/route/route.go b/vendor/github.com/prometheus/common/route/route.go index 1c41b03af..fb337077a 100644 --- a/vendor/github.com/prometheus/common/route/route.go +++ b/vendor/github.com/prometheus/common/route/route.go @@ -75,6 +75,11 @@ func (r *Router) Get(path string, h http.HandlerFunc) { r.rtr.GET(r.prefix+path, handle(h)) } +// Options registers a new OPTIONS route. +func (r *Router) Options(path string, h http.HandlerFunc) { + r.rtr.OPTIONS(r.prefix+path, handle(h)) +} + // Del registers a new DELETE route. func (r *Router) Del(path string, h http.HandlerFunc) { r.rtr.DELETE(r.prefix+path, handle(h)) diff --git a/vendor/vendor.json b/vendor/vendor.json index a4999ab1e..fa7ab463b 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -174,8 +174,8 @@ }, { "path": "github.com/prometheus/common/route", - "revision": "4fdc91a58c9d3696b982e8a680f4997403132d44", - "revisionTime": "2015-10-26T12:04:34+01:00" + "revision": "14ca1097bbe21584194c15e391a9dab95ad42a59", + "revisionTime": "2016-01-25T23:57:51+01:00" }, { "path": "github.com/prometheus/procfs", diff --git a/web/api/legacy/api.go b/web/api/legacy/api.go index 40aede464..cdc7e31e3 100644 --- a/web/api/legacy/api.go +++ b/web/api/legacy/api.go @@ -34,6 +34,12 @@ type API struct { // Register registers the handler for the various endpoints below /api. func (api *API) Register(router *route.Router) { + // List all the endpoints here instead of using a wildcard route because we + // would otherwise handle /api/v1 as well. + router.Options("/query", handle("options", api.Options)) + router.Options("/query_range", handle("options", api.Options)) + router.Options("/metrics", handle("options", api.Options)) + router.Get("/query", handle("query", api.Query)) router.Get("/query_range", handle("query_range", api.QueryRange)) router.Get("/metrics", handle("metrics", api.Metrics)) diff --git a/web/api/legacy/query.go b/web/api/legacy/query.go index 8c94f6ecd..0370394ad 100644 --- a/web/api/legacy/query.go +++ b/web/api/legacy/query.go @@ -31,7 +31,7 @@ import ( // Enables cross-site script calls. func setAccessControlHeaders(w http.ResponseWriter) { w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, Origin") - w.Header().Set("Access-Control-Allow-Methods", "GET") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Expose-Headers", "Date") } @@ -61,6 +61,12 @@ func parseDuration(d string) (time.Duration, error) { return time.Duration(dFloat * float64(time.Second/time.Nanosecond)), nil } +// Options handles OPTIONS requests to /api/... endpoints. +func (api *API) Options(w http.ResponseWriter, r *http.Request) { + setAccessControlHeaders(w) + w.WriteHeader(http.StatusNoContent) +} + // Query handles the /api/query endpoint. func (api *API) Query(w http.ResponseWriter, r *http.Request) { setAccessControlHeaders(w) diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 554bd5273..8acc9c7c9 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -38,6 +38,13 @@ const ( errorBadData = "bad_data" ) +var corsHeaders = map[string]string{ + "Access-Control-Allow-Headers": "Accept, Authorization, Content-Type, Origin", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Access-Control-Expose-Headers": "Date", +} + type apiError struct { typ errorType err error @@ -56,10 +63,9 @@ type response struct { // Enables cross-site script calls. func setCORS(w http.ResponseWriter) { - w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, Origin") - w.Header().Set("Access-Control-Allow-Methods", "GET") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Expose-Headers", "Date") + for h, v := range corsHeaders { + w.Header().Set(h, v) + } } type apiFunc func(r *http.Request) (interface{}, *apiError) @@ -91,8 +97,10 @@ func (api *API) Register(r *route.Router) { setCORS(w) if data, err := f(r); err != nil { respondError(w, err, data) - } else { + } else if data != nil { respond(w, data) + } else { + w.WriteHeader(http.StatusNoContent) } }) return prometheus.InstrumentHandler(name, httputil.CompressionHandler{ @@ -100,6 +108,8 @@ func (api *API) Register(r *route.Router) { }) } + r.Options("/*path", instr("options", api.options)) + r.Get("/query", instr("query", api.query)) r.Get("/query_range", instr("query_range", api.queryRange)) @@ -114,6 +124,10 @@ type queryData struct { Result model.Value `json:"result"` } +func (api *API) options(r *http.Request) (interface{}, *apiError) { + return nil, nil +} + func (api *API) query(r *http.Request) (interface{}, *apiError) { var ts model.Time if t := r.FormValue("time"); t != "" { @@ -255,7 +269,7 @@ func (api *API) dropSeries(r *http.Request) (interface{}, *apiError) { func respond(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) b, err := json.Marshal(&response{ Status: statusSuccess, diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go index 4892de738..2cc8e467c 100644 --- a/web/api/v1/api_test.go +++ b/web/api/v1/api_test.go @@ -503,3 +503,32 @@ func TestParseDuration(t *testing.T) { } } } + +func TestOptionsMethod(t *testing.T) { + r := route.New() + api := &API{} + api.Register(r) + + s := httptest.NewServer(r) + defer s.Close() + + req, err := http.NewRequest("OPTIONS", s.URL+"/any_path", nil) + if err != nil { + t.Fatalf("Error creating OPTIONS request: %s", err) + } + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Error executing OPTIONS request: %s", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("Expected status %d, got %d", http.StatusNoContent, resp.StatusCode) + } + + for h, v := range corsHeaders { + if resp.Header.Get(h) != v { + t.Fatalf("Expected %q for header %q, got %q", v, h, resp.Header.Get(h)) + } + } +}