From a76f9ef0669f1857d97f9933f55da297da59f829 Mon Sep 17 00:00:00 2001 From: MochiGem Date: Mon, 27 Jul 2026 17:55:45 +0900 Subject: [PATCH] Fix URL Parameter loss when r.URL.Path is rewritten by middleware (#1) --- context.go | 59 ++++++++++++++++++++++++ go.mod | 3 ++ mux.go | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++ mux_test.go | 44 ++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 context.go create mode 100644 go.mod create mode 100644 mux.go create mode 100644 mux_test.go diff --git a/context.go b/context.go new file mode 100644 index 0000000..16c6998 --- /dev/null +++ b/context.go @@ -0,0 +1,59 @@ +package chi + +import ( + "context" + "net/http" +) + +type contextKey struct { + name string +} + +var RouteCtxKey = &contextKey{"RouteContext"} + +type Context struct { + Routes Routes + + // Routing path tracking + RoutePath string + + // URLParams key-value pairs + URLParams RouteParams +} + +type RouteParams struct { + Keys []string + Values []string +} + +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +func (s *RouteParams) Get(key string) string { + for i, k := range s.Keys { + if k == key { + return s.Values[i] + } + } + return "" +} + +func NewRouteContext() *Context { + return &Context{} +} + +func RouteContext(ctx context.Context) *Context { + if rctx, ok := ctx.Value(RouteCtxKey).(*Context); ok { + return rctx + } + return nil +} + +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParams.Get(key) + } + return "" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2cfe9f7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/madalynerlge2/chi + +go 1.20 diff --git a/mux.go b/mux.go new file mode 100644 index 0000000..abd335e --- /dev/null +++ b/mux.go @@ -0,0 +1,126 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +type Routes interface { + Routes() []Route +} + +type Route interface{} + +type Router interface { + http.Handler + Routes + Use(middlewares ...func(http.Handler) http.Handler) + Get(pattern string, handlerFn http.HandlerFunc) + Handle(pattern string, handler http.Handler) +} + +type Mux struct { + middlewares []func(http.Handler) http.Handler + routes []routeEntry +} + +type routeEntry struct { + pattern string + handler http.Handler +} + +func NewRouter() *Mux { + return &Mux{} +} + +func (m *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + m.middlewares = append(m.middlewares, middlewares...) +} + +func (m *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + m.Handle(pattern, handlerFn) +} + +func (m *Mux) Handle(pattern string, handler http.Handler) { + m.routes = append(m.routes, routeEntry{ + pattern: pattern, + handler: handler, + }) +} + +func (m *Mux) Routes() []Route { + return nil +} + +func (m *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + if rctx == nil { + rctx = NewRouteContext() + rctx.RoutePath = r.URL.Path + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + } + + // Chain middlewares + var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + m.routeHTTP(w, r) + }) + + for i := len(m.middlewares) - 1; i >= 0; i-- { + handler = m.middlewares[i](handler) + } + + handler.ServeHTTP(w, r) +} + +func (m *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + if rctx != nil { + // CRITICAL FIX FOR ISSUE #1: + // Synchronize RoutePath if middleware mutated r.URL.Path + if rctx.RoutePath != r.URL.Path { + rctx.RoutePath = r.URL.Path + } + } + + currentPath := r.URL.Path + if rctx != nil && rctx.RoutePath != "" { + currentPath = rctx.RoutePath + } + + for _, entry := range m.routes { + params, ok := matchPattern(entry.pattern, currentPath) + if ok { + if rctx != nil { + for k, v := range params { + rctx.URLParams.Add(k, v) + } + } + entry.handler.ServeHTTP(w, r) + return + } + } + + http.NotFound(w, r) +} + +func matchPattern(pattern, path string) (map[string]string, bool) { + patParts := strings.Split(strings.Trim(pattern, "/"), "/") + pathParts := strings.Split(strings.Trim(path, "/"), "/") + + if len(patParts) != len(pathParts) { + return nil, false + } + + params := make(map[string]string) + for i := 0; i < len(patParts); i++ { + if strings.HasPrefix(patParts[i], "{") && strings.HasSuffix(patParts[i], "}") { + paramName := patParts[i][1 : len(patParts[i])-1] + params[paramName] = pathParts[i] + } else if patParts[i] != pathParts[i] { + return nil, false + } + } + + return params, true +} diff --git a/mux_test.go b/mux_test.go new file mode 100644 index 0000000..517a3b8 --- /dev/null +++ b/mux_test.go @@ -0,0 +1,44 @@ +package chi_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/madalynerlge2/chi" +) + +func TestMiddlewarePathRewriteURLParams(t *testing.T) { + r := chi.NewRouter() + + // Middleware that rewrites the path from /legacy/123 to /users/123 + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/legacy/123" { + req.URL.Path = "/users/123" + } + next.ServeHTTP(w, req) + }) + }) + + r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) { + id := chi.URLParam(req, "id") + if id != "123" { + t.Errorf("expected URL param 'id' to be '123', got '%s'", id) + } + w.Write([]byte("ok")) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + res, err := http.Get(ts.URL + "/legacy/123") + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +}