Skip to content

Commit dc0728c

Browse files
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. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c82d515 commit dc0728c

2 files changed

Lines changed: 71 additions & 4 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package httpapi
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"go.uber.org/zap/zaptest"
11+
12+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/config"
13+
"github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts"
14+
)
15+
16+
// registryIDRecorder records the registry id each registry handler forwards.
17+
type registryIDRecorder struct {
18+
*MockServerController
19+
got string
20+
}
21+
22+
func (c *registryIDRecorder) SearchRegistryServers(id, _, _ string, _ int) ([]interface{}, *contracts.RegistryCacheInfo, error) {
23+
c.got = id
24+
return []interface{}{}, nil, nil
25+
}
26+
27+
func (c *registryIDRecorder) RefreshRegistryCache(id string) (int, error) {
28+
c.got = id
29+
return 0, nil
30+
}
31+
32+
func (c *registryIDRecorder) RemoveRegistrySourceRef(id string) (*config.RegistryEntry, *contracts.RegistryAddError, error) {
33+
c.got = id
34+
return &config.RegistryEntry{ID: id}, nil, nil
35+
}
36+
37+
func (c *registryIDRecorder) EditRegistrySourceRef(id, _, _, _ string) (*config.RegistryEntry, *contracts.RegistryAddError, error) {
38+
c.got = id
39+
return &config.RegistryEntry{ID: id}, nil, nil
40+
}
41+
42+
// A custom registry id is user-chosen (add-source --id) and may contain
43+
// characters the CLI and Web UI percent-encode. chi routes on RawPath, so the
44+
// {id} param arrives encoded and must be decoded before the exact-match lookup,
45+
// as handleAddFromRegistry already does.
46+
func TestRegistryRoutes_DecodeIDParam(t *testing.T) {
47+
routes := []struct{ method, path, body string }{
48+
{http.MethodGet, "/api/v1/registries/acme%3Ainternal/servers", ""},
49+
{http.MethodPost, "/api/v1/registries/acme%3Ainternal/refresh", ""},
50+
{http.MethodDelete, "/api/v1/registries/acme%3Ainternal", ""},
51+
{http.MethodPut, "/api/v1/registries/acme%3Ainternal", `{"name":"Acme"}`},
52+
}
53+
for _, rt := range routes {
54+
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
55+
ctrl := &registryIDRecorder{MockServerController: &MockServerController{}}
56+
srv := NewServer(ctrl, zaptest.NewLogger(t).Sugar(), nil)
57+
58+
req := httptest.NewRequest(rt.method, rt.path, strings.NewReader(rt.body))
59+
req.Header.Set("X-API-Key", mockControllerAPIKey)
60+
w := httptest.NewRecorder()
61+
srv.ServeHTTP(w, req)
62+
63+
assert.Equal(t, http.StatusOK, w.Code, "body=%s", w.Body.String())
64+
assert.Equal(t, "acme:internal", ctrl.got)
65+
})
66+
}
67+
}

‎internal/httpapi/server.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6417,7 +6417,7 @@ func (s *Server) handleListRegistries(w http.ResponseWriter, r *http.Request) {
64176417
// @Security ApiKeyQuery
64186418
// @Router /api/v1/registries/{id}/servers [get]
64196419
func (s *Server) handleSearchRegistryServers(w http.ResponseWriter, r *http.Request) {
6420-
registryID := chi.URLParam(r, "id")
6420+
registryID := decodePathParam(chi.URLParam(r, "id"))
64216421
if registryID == "" {
64226422
s.writeError(w, r, http.StatusBadRequest, "Registry ID is required")
64236423
return
@@ -6673,7 +6673,7 @@ func (s *Server) handleAddRegistrySource(w http.ResponseWriter, r *http.Request)
66736673
// @Security ApiKeyQuery
66746674
// @Router /api/v1/registries/{id} [delete]
66756675
func (s *Server) handleRemoveRegistrySource(w http.ResponseWriter, r *http.Request) {
6676-
registryID := chi.URLParam(r, "id")
6676+
registryID := decodePathParam(chi.URLParam(r, "id"))
66776677
if registryID == "" {
66786678
s.writeError(w, r, http.StatusBadRequest, "Registry ID is required")
66796679
return
@@ -6712,7 +6712,7 @@ func (s *Server) handleRemoveRegistrySource(w http.ResponseWriter, r *http.Reque
67126712
// @Security ApiKeyQuery
67136713
// @Router /api/v1/registries/{id} [put]
67146714
func (s *Server) handleEditRegistrySource(w http.ResponseWriter, r *http.Request) {
6715-
registryID := chi.URLParam(r, "id")
6715+
registryID := decodePathParam(chi.URLParam(r, "id"))
67166716
if registryID == "" {
67176717
s.writeError(w, r, http.StatusBadRequest, "Registry ID is required")
67186718
return
@@ -6751,7 +6751,7 @@ func (s *Server) handleEditRegistrySource(w http.ResponseWriter, r *http.Request
67516751
// @Failure 500 {object} contracts.ErrorResponse "Failed to refresh registry cache"
67526752
// @Router /api/v1/registries/{id}/refresh [post]
67536753
func (s *Server) handleRefreshRegistryCache(w http.ResponseWriter, r *http.Request) {
6754-
registryID := chi.URLParam(r, "id")
6754+
registryID := decodePathParam(chi.URLParam(r, "id"))
67556755
if registryID == "" {
67566756
s.writeError(w, r, http.StatusBadRequest, "Registry ID is required")
67576757
return

0 commit comments

Comments
 (0)