From 191aec68bd86dba8a6ca3c251de3b3124838ad89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusufhan=20Sa=C3=A7ak?= Date: Sat, 11 Jul 2026 13:36:10 +0300 Subject: [PATCH] fix(api): allow PATCH in CORS so browsers can call the status endpoints The status endpoints (PATCH /v0/servers/{name}/status and the per-version variant) are served with PATCH, but the CORS middleware's AllowedMethods never listed PATCH, so a cross-origin browser preflight for them returns without the allow headers and the browser blocks the request. Added PATCH to the allowed methods. The existing CORS test is a black-box placeholder that can't reach the wrapped handler (its own comments say as much), which is why this slipped through. Added a small Handler() accessor and a real preflight test asserting every routed method including PATCH is allowed. --- internal/api/cors_preflight_test.go | 62 +++++++++++++++++++++++++++++ internal/api/server.go | 7 ++++ 2 files changed, 69 insertions(+) create mode 100644 internal/api/cors_preflight_test.go diff --git a/internal/api/cors_preflight_test.go b/internal/api/cors_preflight_test.go new file mode 100644 index 000000000..8ccec81d9 --- /dev/null +++ b/internal/api/cors_preflight_test.go @@ -0,0 +1,62 @@ +package api_test + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/hex" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/modelcontextprotocol/registry/internal/api" + v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0" + "github.com/modelcontextprotocol/registry/internal/config" + "github.com/modelcontextprotocol/registry/internal/telemetry" +) + +// TestCORSPreflightAllowedMethods checks that the CORS layer permits every HTTP +// method the API actually routes. The status endpoints +// (PATCH /v0/servers/{name}/status and .../versions/{version}/status) are served +// with PATCH, so a cross-origin browser preflight for PATCH has to be allowed or +// the browser refuses to send the real request. Guards against a routed method +// being missing from AllowedMethods. +func TestCORSPreflightAllowedMethods(t *testing.T) { + seed := make([]byte, ed25519.SeedSize) + _, err := rand.Read(seed) + require.NoError(t, err) + + cfg := config.NewConfig() + cfg.JWTPrivateKey = hex.EncodeToString(seed) + + shutdownTelemetry, metrics, err := telemetry.InitMetrics("test") + require.NoError(t, err) + defer func() { _ = shutdownTelemetry(nil) }() + + // registryService is nil on purpose: a CORS preflight is answered by the + // middleware before any route handler runs, so business logic is never hit. + srv := api.NewServer(cfg, nil, metrics, &v0.VersionBody{Version: "test"}) + + for _, method := range []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + } { + t.Run(method, func(t *testing.T) { + req := httptest.NewRequest(http.MethodOptions, "/v0/servers/example/status", nil) + req.Header.Set("Origin", "https://example.com") + req.Header.Set("Access-Control-Request-Method", method) + + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + allow := w.Header().Get("Access-Control-Allow-Methods") + assert.Contains(t, allow, method, + "CORS preflight for %s should be allowed, got Access-Control-Allow-Methods=%q", method, allow) + }) + } +} diff --git a/internal/api/server.go b/internal/api/server.go index 9114067ac..3cb8a7f10 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -109,6 +109,7 @@ func NewServer(cfg *config.Config, registryService service.RegistryService, metr http.MethodGet, http.MethodPost, http.MethodPut, + http.MethodPatch, http.MethodDelete, http.MethodOptions, }, @@ -147,6 +148,12 @@ func NewServer(cfg *config.Config, registryService service.RegistryService, metr return server } +// Handler returns the fully wrapped HTTP handler (middleware stack plus routes). +// Exposed so tests can exercise the middleware, e.g. CORS, without binding a port. +func (s *Server) Handler() http.Handler { + return s.server.Handler +} + // Start begins listening for incoming HTTP requests func (s *Server) Start() error { log.Printf("HTTP server starting on %s", s.config.ServerAddress)