From 3a39b665acebb871ed721d4f510e792addeafc95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusufhan=20Sa=C3=A7ak?= <67666222+JosephDoUrden@users.noreply.github.com> Date: Sun, 12 Jul 2026 21:53:13 +0300 Subject: [PATCH] fix(api): allow PATCH in CORS so browsers can call the status endpoints (#1436) The status endpoints (PATCH /v0/servers/{name}/status and the per-version one) are served with PATCH, but the CORS middleware's AllowedMethods lists GET/POST/PUT/DELETE/OPTIONS and never PATCH. So a cross-origin browser preflight for those endpoints comes back without the allow headers and the browser blocks the actual request. One-line fix: add PATCH to the allowed methods. While here, the existing CORS test turned out to be a placeholder that never reaches the real wrapped handler (its own comments say so), which is why this went unnoticed. Added a small Handler() accessor and a proper preflight test that asserts each 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 0000000..8ccec81 --- /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 9114067..3cb8a7f 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)