From 686c9a21ff6b6a670dcf9c42be4bfc5b7181ad96 Mon Sep 17 00:00:00 2001 From: andriibeee <154226341+andriibeee@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:57:13 +0200 Subject: [PATCH] lib/httpserver: handle preflight HTTP requests properly Previously OPTIONS HTTP requests for CORS preflight checks would trigger the original request handler. This pull request fixes that behavior to align with https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5563 --- docs/victoriametrics/changelog/CHANGELOG.md | 1 + lib/httpserver/httpserver.go | 8 ++++ lib/httpserver/httpserver_test.go | 53 +++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/docs/victoriametrics/changelog/CHANGELOG.md b/docs/victoriametrics/changelog/CHANGELOG.md index e094f6cfb9..9e0c85a592 100644 --- a/docs/victoriametrics/changelog/CHANGELOG.md +++ b/docs/victoriametrics/changelog/CHANGELOG.md @@ -26,6 +26,7 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel ## tip +* FEATURE: all VictoriaMetrics components: implement proper CORS preflight handling by responding 204 No Content to HTTP OPTIONS requests. See [#5563](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5563). * FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): add `access_log` configuration option for each user that will log requests to stdout, and support filtering by HTTP status codes. See more in [docs](https://docs.victoriametrics.com/victoriametrics/vmauth/#access-log). See [#5936](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/5936). * FEATURE: [vmalert](https://docs.victoriametrics.com/victoriametrics/vmalert/): support negative values for the group `eval_offset` option, which allows starting group evaluation at `groupInterval-abs(eval_offset)` within `[0...groupInterval]`. See [#10424](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10424). * FEATURE: [vmsingle](https://docs.victoriametrics.com/victoriametrics/single-server-victoriametrics/) and `vmselect` in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): Disable `/graphite/tags/tagSeries` and `/graphite/tags/tagMultiSeries` for Graphite tag registration since it is unlikely it is used in context of VictoriaMetrics. See [10544](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10544). diff --git a/lib/httpserver/httpserver.go b/lib/httpserver/httpserver.go index 3378308b15..96d6548eb6 100644 --- a/lib/httpserver/httpserver.go +++ b/lib/httpserver/httpserver.go @@ -357,6 +357,12 @@ func handlerWrapper(w http.ResponseWriter, r *http.Request, rh RequestHandler) { r.URL.Path = path } + if r.Method == http.MethodOptions { + EnableCORS(w, r) + w.WriteHeader(http.StatusNoContent) + return + } + w = &responseWriterWithAbort{ ResponseWriter: w, } @@ -511,6 +517,8 @@ func EnableCORS(w http.ResponseWriter, _ *http.Request) { return } w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") } func pprofHandler(profileName string, w http.ResponseWriter, r *http.Request) { diff --git a/lib/httpserver/httpserver_test.go b/lib/httpserver/httpserver_test.go index 2de2265932..c1adb7fcba 100644 --- a/lib/httpserver/httpserver_test.go +++ b/lib/httpserver/httpserver_test.go @@ -144,6 +144,59 @@ func TestAuthKeyMetrics(t *testing.T) { tstWithOutAuthKey("wrong", "wrong", 401) } +func TestHandlerWrapperOptionsRequest(t *testing.T) { + handlerCalled := false + rh := func(_ http.ResponseWriter, _ *http.Request) bool { + handlerCalled = true + return true + } + + f := func(t *testing.T, name string, corsDisabled bool, expectAllowOrigin bool) { + t.Helper() + handlerCalled = false + + origDisableCORS := *disableCORS + *disableCORS = corsDisabled + defer func() { + *disableCORS = origDisableCORS + }() + + req := httptest.NewRequest(http.MethodOptions, "/api/v1/query_range", nil) + w := httptest.NewRecorder() + + handlerWrapper(w, req, rh) + + res := w.Result() + _ = res.Body.Close() + + if res.StatusCode != http.StatusNoContent { + t.Fatalf("%s: unexpected status code; got %d; want %d", name, res.StatusCode, http.StatusNoContent) + } + if handlerCalled { + t.Fatalf("%s: request handler must not be called for OPTIONS requests", name) + } + if got := res.Header.Get("Access-Control-Allow-Methods"); got != "*" { + t.Fatalf("%s: unexpected Access-Control-Allow-Methods; got %q; want %q", name, got, "*") + } + wantHeaders := "*" + if got := res.Header.Get("Access-Control-Allow-Headers"); got != wantHeaders { + t.Fatalf("%s: unexpected Access-Control-Allow-Headers; got %q; want %q", name, got, wantHeaders) + } + if expectAllowOrigin { + if got := res.Header.Get("Access-Control-Allow-Origin"); got != "*" { + t.Fatalf("%s: unexpected Access-Control-Allow-Origin; got %q; want %q", name, got, "*") + } + } else { + if got := res.Header.Get("Access-Control-Allow-Origin"); got != "" { + t.Fatalf("%s: Access-Control-Allow-Origin must be empty when CORS is disabled; got %q", name, got) + } + } + } + + f(t, "cors enabled", false, true) + f(t, "cors disabled", true, false) +} + func TestHandlerWrapper(t *testing.T) { const hstsHeader = "foo" const frameOptionsHeader = "bar"