From 3a908d22a12712e78f0e99eea4cf01dd87943344 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sat, 24 Oct 2026 17:40:34 -0400 Subject: [PATCH] fix(httpapi): percent-decode the registry {id} on search, refresh, edit and remove A custom registry id is user-chosen (add-source --id) and may contain characters the CLI (url.PathEscape) and Web UI (encodeURIComponent) percent-encode, such as ':' or '/'. chi routes on RawPath, so the {id} param arrived encoded and the exact-match lookup missed: the registry could be added but not searched, refreshed, edited or removed. handleAddFromRegistry already decodes; apply the same decodePathParam in the other four registry handlers. --- internal/httpapi/registry_id_decode_test.go | 67 +++++++++++++++++++++ internal/httpapi/server.go | 8 +-- 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 internal/httpapi/registry_id_decode_test.go diff --git a/internal/httpapi/registry_id_decode_test.go b/internal/httpapi/registry_id_decode_test.go new file mode 100644 index 000000000..001208515 --- /dev/null +++ b/internal/httpapi/registry_id_decode_test.go @@ -0,0 +1,67 @@ +package httpapi + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zaptest" + + "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts" +) + +// registryIDRecorder records the registry id each registry handler forwards. +type registryIDRecorder struct { + *MockServerController + got string +} + +func (c *registryIDRecorder) SearchRegistryServers(id, _, _ string, _ int) ([]interface{}, *contracts.RegistryCacheInfo, error) { + c.got = id + return []interface{}{}, nil, nil +} + +func (c *registryIDRecorder) RefreshRegistryCache(id string) (int, error) { + c.got = id + return 0, nil +} + +func (c *registryIDRecorder) RemoveRegistrySourceRef(id string) (*config.RegistryEntry, *contracts.RegistryAddError, error) { + c.got = id + return &config.RegistryEntry{ID: id}, nil, nil +} + +func (c *registryIDRecorder) EditRegistrySourceRef(id, _, _, _ string) (*config.RegistryEntry, *contracts.RegistryAddError, error) { + c.got = id + return &config.RegistryEntry{ID: id}, nil, nil +} + +// A custom registry id is user-chosen (add-source --id) and may contain +// characters the CLI and Web UI percent-encode. chi routes on RawPath, so the +// {id} param arrives encoded and must be decoded before the exact-match lookup, +// as handleAddFromRegistry already does. +func TestRegistryRoutes_DecodeIDParam(t *testing.T) { + routes := []struct{ method, path, body string }{ + {http.MethodGet, "/api/v1/registries/acme%3Ainternal/servers", ""}, + {http.MethodPost, "/api/v1/registries/acme%3Ainternal/refresh", ""}, + {http.MethodDelete, "/api/v1/registries/acme%3Ainternal", ""}, + {http.MethodPut, "/api/v1/registries/acme%3Ainternal", `{"name":"Acme"}`}, + } + for _, rt := range routes { + t.Run(rt.method+" "+rt.path, func(t *testing.T) { + ctrl := ®istryIDRecorder{MockServerController: &MockServerController{}} + srv := NewServer(ctrl, zaptest.NewLogger(t).Sugar(), nil) + + req := httptest.NewRequest(rt.method, rt.path, strings.NewReader(rt.body)) + req.Header.Set("X-API-Key", mockControllerAPIKey) + w := httptest.NewRecorder() + srv.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String()) + assert.Equal(t, "acme:internal", ctrl.got) + }) + } +} diff --git a/internal/httpapi/server.go b/internal/httpapi/server.go index 0831b1cdc..0d1037369 100644 --- a/internal/httpapi/server.go +++ b/internal/httpapi/server.go @@ -6417,7 +6417,7 @@ func (s *Server) handleListRegistries(w http.ResponseWriter, r *http.Request) { // @Security ApiKeyQuery // @Router /api/v1/registries/{id}/servers [get] func (s *Server) handleSearchRegistryServers(w http.ResponseWriter, r *http.Request) { - registryID := chi.URLParam(r, "id") + registryID := decodePathParam(chi.URLParam(r, "id")) if registryID == "" { s.writeError(w, r, http.StatusBadRequest, "Registry ID is required") return @@ -6673,7 +6673,7 @@ func (s *Server) handleAddRegistrySource(w http.ResponseWriter, r *http.Request) // @Security ApiKeyQuery // @Router /api/v1/registries/{id} [delete] func (s *Server) handleRemoveRegistrySource(w http.ResponseWriter, r *http.Request) { - registryID := chi.URLParam(r, "id") + registryID := decodePathParam(chi.URLParam(r, "id")) if registryID == "" { s.writeError(w, r, http.StatusBadRequest, "Registry ID is required") return @@ -6712,7 +6712,7 @@ func (s *Server) handleRemoveRegistrySource(w http.ResponseWriter, r *http.Reque // @Security ApiKeyQuery // @Router /api/v1/registries/{id} [put] func (s *Server) handleEditRegistrySource(w http.ResponseWriter, r *http.Request) { - registryID := chi.URLParam(r, "id") + registryID := decodePathParam(chi.URLParam(r, "id")) if registryID == "" { s.writeError(w, r, http.StatusBadRequest, "Registry ID is required") return @@ -6751,7 +6751,7 @@ func (s *Server) handleEditRegistrySource(w http.ResponseWriter, r *http.Request // @Failure 500 {object} contracts.ErrorResponse "Failed to refresh registry cache" // @Router /api/v1/registries/{id}/refresh [post] func (s *Server) handleRefreshRegistryCache(w http.ResponseWriter, r *http.Request) { - registryID := chi.URLParam(r, "id") + registryID := decodePathParam(chi.URLParam(r, "id")) if registryID == "" { s.writeError(w, r, http.StatusBadRequest, "Registry ID is required") return