diff --git a/context.go b/context.go new file mode 100644 index 0000000..50a85bb --- /dev/null +++ b/context.go @@ -0,0 +1,169 @@ +package chi + +import ( + "context" + "net/http" + "strings" +) + +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + +var ( + // RouteCtxKey is the context.Context key to store the request context. + RouteCtxKey = &contextKey{"RouteContext"} +) + +// Context is the default routing context set on the root node of a +// request context to track route patterns, URL parameters and +// an optional routing path. +type Context struct { + Routes Routes + + // parentCtx is the parent of this one, for using Context as a + // context.Context directly. This is an optimization that saves + // 1 allocation. + parentCtx context.Context + + // Routing path/method override used during the route search. + // See Mux#routeHTTP method. + RoutePath string + RouteMethod string + + // routePathSource tracks the r.URL.Path value that was used to set + // RoutePath. When a middleware rewrites r.URL.Path, routeHTTP compares + // r.URL.Path against routePathSource to detect the change and + // recompute RoutePath accordingly. + routePathSource string + + // mountPrefix records the mount pattern of the current sub-router + // (including trailing slash), so that routeHTTP can strip it from + // r.URL.Path when recomputing RoutePath after a middleware rewrite. + mountPrefix string + + // URLParams are the stack of routeParams captured during the + // routing lifecycle across a stack of sub-routers. + URLParams RouteParams + + // Route parameters matched for the current sub-router. It is + // intentionally unexported so it can't be tampered. + routeParams RouteParams + + // The endpoint routing pattern that matched the request URI path + // or `RoutePath` of the current sub-router. This value will update + // during the lifecycle of a request passing through a stack of + // sub-routers. + routePattern string + + // Routing pattern stack throughout the lifecycle of the request, + // across all connected routers. It is a record of all matching + // patterns across a stack of sub-routers. + RoutePatterns []string + + methodsAllowed []methodTyp // allowed methods in case of a 405 + methodNotAllowed bool +} + +// Reset a routing context to its initial state. +func (x *Context) Reset() { + x.Routes = nil + x.RoutePath = "" + x.RouteMethod = "" + x.routePathSource = "" + x.mountPrefix = "" + x.RoutePatterns = x.RoutePatterns[:0] + x.URLParams.Keys = x.URLParams.Keys[:0] + x.URLParams.Values = x.URLParams.Values[:0] + + x.routePattern = "" + x.routeParams.Keys = x.routeParams.Keys[:0] + x.routeParams.Values = x.routeParams.Values[:0] + x.methodNotAllowed = false + x.methodsAllowed = x.methodsAllowed[:0] + x.parentCtx = nil +} + +// URLParam returns the corresponding URL parameter value from the request +// routing context. +func (x *Context) URLParam(key string) string { + for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { + if x.URLParams.Keys[k] == key { + return x.URLParams.Values[k] + } + } + return "" +} + +// RoutePattern builds the routing pattern string for the particular +// request, at the particular point during routing. This means, the value +// will change throughout the execution of a request in a router. That is +// why it's advised to only use this value after calling the next handler. +func (x *Context) RoutePattern() string { + if x == nil { + return "" + } + routePattern := strings.Join(x.RoutePatterns, "") + routePattern = replaceWildcards(routePattern) + if routePattern != "/" { + routePattern = strings.TrimSuffix(routePattern, "//") + routePattern = strings.TrimSuffix(routePattern, "/") + } + return routePattern +} + +// replaceWildcards takes a route pattern and replaces all occurrences of +// "/*/" with "/". It iteratively runs until no wildcards remain to +// correctly handle consecutive wildcards. +func replaceWildcards(p string) string { + for strings.Contains(p, "/*/") { + p = strings.ReplaceAll(p, "/*/", "/") + } + return p +} + +// RouteParams is a structure to track URL routing parameters efficiently. +type RouteParams struct { + Keys, Values []string +} + +// Add will append a URL parameter to the end of the route param +func (s *RouteParams) Add(key, value string) { + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "chi context value " + k.name +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..609ae15 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/madalynerlge2/chi + +go 1.22 \ No newline at end of file diff --git a/main.go b/main.go deleted file mode 100644 index 49f4dee..0000000 --- a/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "fmt" - -func main() { - fmt.Println("Hello, Bounty Hunter!") -} diff --git a/method.go b/method.go new file mode 100644 index 0000000..9de9419 --- /dev/null +++ b/method.go @@ -0,0 +1,50 @@ +package chi + +import ( + "strings" +) + +// methodTyp is a bitfield of supported HTTP methods. +type methodTyp uint + +const ( + mCONNECT methodTyp = 1 << iota + mDELETE + mGET + mHEAD + mOPTIONS + mPATCH + mPOST + mPUT + mTRACE + mALL = mCONNECT | mDELETE | mGET | mHEAD | mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + mSTUB = 1 << 15 + mSTUBOR = mSTUB | mALL +) + +var methodMap = map[string]methodTyp{ + "CONNECT": mCONNECT, + "DELETE": mDELETE, + "GET": mGET, + "HEAD": mHEAD, + "OPTIONS": mOPTIONS, + "PATCH": mPATCH, + "POST": mPOST, + "PUT": mPUT, + "TRACE": mTRACE, +} + +// RegisterMethod adds support for custom HTTP method handlers, available via +// Router#Method and Router#MethodFunc. +func RegisterMethod(method string) { + method = strings.ToUpper(method) + if method == "" { + return + } + if _, ok := methodMap[method]; ok { + return + } + // Find the next available bit position + nextBit := methodTyp(1) << uint(len(methodMap)) + methodMap[method] = nextBit +} \ No newline at end of file diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..fa937c6 --- /dev/null +++ b/middleware.go @@ -0,0 +1,54 @@ +package chi + +import ( + "net/http" + "strings" +) + +// Middlewares is a list of middleware handler functions +type Middlewares []func(http.Handler) http.Handler + +// Chain returns a Middlewares Chain that can be used to wrap a handler. +// The chain is built in reverse order so that the first middleware in the +// slice is the outermost wrapper (runs first). +func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { + return Middlewares(middlewares) +} + +// Handler wraps the given handler with the middleware chain. +func (mws Middlewares) Handler(h http.Handler) http.Handler { + if len(mws) == 0 { + return h + } + for i := len(mws) - 1; i >= 0; i-- { + h = mws[i](h) + } + return h +} + +// HandlerFunc wraps the given handler function with the middleware chain. +func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { + return mws.Handler(http.HandlerFunc(h)) +} + +// methodNotAllowedHandler returns a 405 handler with the given allowed methods. +func methodNotAllowedHandler(methodsAllowed ...methodTyp) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + allowed := make([]string, 0, len(methodsAllowed)) + for _, m := range methodsAllowed { + allowed = append(allowed, methodTypString(m)) + } + w.Header().Set("Allow", strings.Join(allowed, ", ")) + w.WriteHeader(http.StatusMethodNotAllowed) + } +} + +// methodTypString converts a methodTyp to its string representation. +func methodTypString(m methodTyp) string { + for s, mt := range methodMap { + if mt == m { + return s + } + } + return "" +} \ No newline at end of file diff --git a/mux.go b/mux.go new file mode 100644 index 0000000..0065c1d --- /dev/null +++ b/mux.go @@ -0,0 +1,525 @@ +package chi + +import ( + "context" + "fmt" + "net/http" + "strings" + "sync" +) + +var _ Router = &Mux{} + +// Mux is a simple HTTP route multiplexer that parses a request path, +// records any URL params, and executes an end handler. It implements +// the http.Handler interface and is friendly with the standard library. +type Mux struct { + // The computed mux handler made of the chained middleware stack and + // the tree router + handler http.Handler + + // The radix trie router + tree *node + + // Custom method not allowed handler + methodNotAllowedHandler http.HandlerFunc + + // A reference to the parent mux used by subrouters when mounting + // to a parent mux + parent *Mux + + // Routing context pool + pool *sync.Pool + + // Custom route not found handler + notFoundHandler http.HandlerFunc + + // The middleware stack + middlewares []func(http.Handler) http.Handler + + // Controls the behaviour of middleware chain generation when a mux + // is registered as an inline group inside another mux. + inline bool +} + +// NewMux returns a newly initialized Mux object that implements the Router +// interface. +func NewMux() *Mux { + mux := &Mux{tree: &node{}, pool: &sync.Pool{}} + mux.pool.New = func() interface{} { + return NewRouteContext() + } + return mux +} + +// ServeHTTP is the single method of the http.Handler interface that makes +// Mux interoperable with the standard library. It uses a sync.Pool to get and +// reuse routing contexts for each request. +func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Ensure the mux has some routes defined on the mux + if mx.handler == nil { + mx.NotFoundHandler().ServeHTTP(w, r) + return + } + + // Check if a routing context already exists from a parent router. + rctx, _ := r.Context().Value(RouteCtxKey).(*Context) + if rctx != nil { + mx.handler.ServeHTTP(w, r) + return + } + + // Fetch a RouteContext object from the sync pool, and call the computed + // mx.handler that is comprised of mx.middlewares + mx.routeHTTP. + // Once the request is finished, reset the routing context and put it back + // into the pool for reuse from another request. + rctx = mx.pool.Get().(*Context) + rctx.Reset() + rctx.Routes = mx + rctx.parentCtx = r.Context() + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation + r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool + mx.handler.ServeHTTP(w, r) + mx.pool.Put(rctx) +} + +// Use appends a middleware handler to the Mux middleware stack. +func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { + if mx.handler != nil { + panic("chi: all middlewares must be defined before routes on a mux") + } + mx.middlewares = append(mx.middlewares, middlewares...) +} + +// Handle adds the route `pattern` that matches any http method to +// execute the `handler` http.Handler. +func (mx *Mux) Handle(pattern string, handler http.Handler) { + if i := strings.IndexAny(pattern, " \t"); i >= 0 { + method, rest := pattern[:i], strings.TrimLeft(pattern[i+1:], " \t") + mx.Method(method, rest, handler) + return + } + + mx.handle(mALL, pattern, handler) +} + +// HandleFunc adds the route `pattern` that matches any http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { + mx.Handle(pattern, handlerFn) +} + +// Method adds the route `pattern` that matches `method` http method to +// execute the `handler` http.Handler. +func (mx *Mux) Method(method, pattern string, handler http.Handler) { + m, ok := methodMap[strings.ToUpper(method)] + if !ok { + panic(fmt.Sprintf("chi: '%s' http method is not supported.", method)) + } + mx.handle(m, pattern, handler) +} + +// MethodFunc adds the route `pattern` that matches `method` http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) MethodFunc(method, pattern string, handlerFn http.HandlerFunc) { + mx.Method(method, pattern, handlerFn) +} + +// Connect adds the route `pattern` that matches a CONNECT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Connect(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mCONNECT, pattern, handlerFn) +} + +// Delete adds the route `pattern` that matches a DELETE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Delete(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mDELETE, pattern, handlerFn) +} + +// Get adds the route `pattern` that matches a GET http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Get(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mGET, pattern, handlerFn) +} + +// Head adds the route `pattern` that matches a HEAD http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Head(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mHEAD, pattern, handlerFn) +} + +// Options adds the route `pattern` that matches an OPTIONS http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Options(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mOPTIONS, pattern, handlerFn) +} + +// Patch adds the route `pattern` that matches a PATCH http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Patch(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPATCH, pattern, handlerFn) +} + +// Post adds the route `pattern` that matches a POST http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Post(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPOST, pattern, handlerFn) +} + +// Put adds the route `pattern` that matches a PUT http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mPUT, pattern, handlerFn) +} + +// Trace adds the route `pattern` that matches a TRACE http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mTRACE, pattern, handlerFn) +} + +// NotFound sets a custom http.HandlerFunc for routing paths that could +// not be found. The default 404 handler is `http.NotFound`. +func (mx *Mux) NotFound(handlerFn http.HandlerFunc) { + // Build NotFound handler chain + m := mx + hFn := handlerFn + if mx.inline && mx.parent != nil { + m = mx.parent + hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP + } + + // Update the notFoundHandler from this point forward + m.notFoundHandler = hFn + m.updateSubRoutes(func(subMux *Mux) { + if subMux.notFoundHandler == nil { + subMux.NotFound(hFn) + } + }) +} + +// MethodNotAllowed sets a custom http.HandlerFunc for routing paths where the +// method is unresolved. The default handler returns a 405 with an empty body. +func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { + // Build MethodNotAllowed handler chain + m := mx + hFn := handlerFn + if mx.inline && mx.parent != nil { + m = mx.parent + hFn = Chain(mx.middlewares...).HandlerFunc(hFn).ServeHTTP + } + + // Update the methodNotAllowedHandler from this point forward + m.methodNotAllowedHandler = hFn + m.updateSubRoutes(func(subMux *Mux) { + if subMux.methodNotAllowedHandler == nil { + subMux.MethodNotAllowed(hFn) + } + }) +} + +// With adds inline middlewares for an endpoint handler. +func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { + // Similarly as in handle(), we must build the mux handler once additional + // middleware registration isn't allowed for this stack, like now. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Copy middlewares from parent inline muxs + var mws Middlewares + if mx.inline { + mws = make(Middlewares, len(mx.middlewares)) + copy(mws, mx.middlewares) + } + mws = append(mws, middlewares...) + + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } + + return im +} + +// Group creates a new inline-Mux with a copy of middleware stack. It's useful +// for a group of handlers along the same routing path that use an additional +// set of middlewares. +func (mx *Mux) Group(fn func(r Router)) Router { + im := mx.With() + if fn != nil { + fn(im) + } + return im +} + +// Route creates a new Mux and mounts it along the `pattern` as a subrouter. +// Effectively, this is a short-hand call to Mount. +func (mx *Mux) Route(pattern string, fn func(r Router)) Router { + if fn == nil { + panic(fmt.Sprintf("chi: attempting to Route() a nil subrouter on '%s'", pattern)) + } + subRouter := NewRouter() + fn(subRouter) + mx.Mount(pattern, subRouter) + return subRouter +} + +// Mount attaches another http.Handler or chi Router as a subrouter along a routing +// path. It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. +func (mx *Mux) Mount(pattern string, handler http.Handler) { + if handler == nil { + panic(fmt.Sprintf("chi: attempting to Mount() a nil handler on '%s'", pattern)) + } + + // Provide runtime safety for ensuring a pattern isn't mounted on an existing + // routing pattern. + if mx.tree.findPattern(pattern+"*") || mx.tree.findPattern(pattern+"/*") { + panic(fmt.Sprintf("chi: attempting to Mount() a handler on an existing path, '%s'", pattern)) + } + + // Assign sub-Router's with the parent not found & method not allowed handler if not specified. + subr, ok := handler.(*Mux) + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { + subr.NotFound(mx.notFoundHandler) + } + if ok && subr.methodNotAllowedHandler == nil && mx.methodNotAllowedHandler != nil { + subr.MethodNotAllowed(mx.methodNotAllowedHandler) + } + + // Record the mount pattern for later use in routePath synchronization + mountPattern := pattern + if mountPattern != "" && mountPattern[len(mountPattern)-1] != '/' { + mountPattern += "/" + } + + mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + rctx := RouteContext(r.Context()) + + // shift the url path past the previous subrouter + rctx.RoutePath = mx.nextRoutePath(rctx) + + // Record the source path and mount prefix so that routeHTTP can + // detect if a middleware has rewritten r.URL.Path and recompute + // RoutePath accordingly. + rctx.routePathSource = r.URL.Path + rctx.mountPrefix = mountPattern + + // reset the wildcard URLParam which connects the subrouter + n := len(rctx.URLParams.Keys) - 1 + if n >= 0 && rctx.URLParams.Keys[n] == "*" && len(rctx.URLParams.Values) > n { + rctx.URLParams.Values[n] = "" + } + + handler.ServeHTTP(w, r) + }) + + if pattern == "" || pattern[len(pattern)-1] != '/' { + mx.handle(mALL|mSTUB, pattern, mountHandler) + mx.handle(mALL|mSTUB, pattern+"/", mountHandler) + pattern += "/" + } + + method := mALL + subroutes, _ := handler.(Router) + if subroutes != nil { + method |= mSTUB + } + n := mx.handle(method, pattern+"*", mountHandler) + + if subroutes != nil { + n.subroutes = subroutes + } +} + +// Routes returns a slice of routing information from the tree, +// useful for traversing available routes of a router. +func (mx *Mux) Routes() []Route { + return mx.tree.routes() +} + +// Middlewares returns a slice of middleware handler functions. +func (mx *Mux) Middlewares() Middlewares { + return mx.middlewares +} + +// Match searches the routing tree for a handler that matches the method/path. +func (mx *Mux) Match(rctx *Context, method, path string) bool { + return mx.Find(rctx, method, path) != "" +} + +// Find searches the routing tree for the pattern that matches +// the method/path. +func (mx *Mux) Find(rctx *Context, method, path string) string { + m, ok := methodMap[method] + if !ok { + return "" + } + + node, _, _ := mx.tree.FindRoute(rctx, m, path) + pattern := rctx.routePattern + + if node != nil { + if node.subroutes == nil { + e := node.endpoints[m] + return e.pattern + } + + rctx.RoutePath = mx.nextRoutePath(rctx) + subPattern := node.subroutes.Find(rctx, method, rctx.RoutePath) + if subPattern == "" { + return "" + } + + pattern = strings.TrimSuffix(pattern, "/*") + pattern += subPattern + } + + return pattern +} + +// NotFoundHandler returns the default Mux 404 responder whenever a route +// cannot be found. +func (mx *Mux) NotFoundHandler() http.HandlerFunc { + if mx.notFoundHandler != nil { + return mx.notFoundHandler + } + return http.NotFound +} + +// MethodNotAllowedHandler returns the default Mux 405 responder whenever +// a method cannot be resolved for a route. +func (mx *Mux) MethodNotAllowedHandler(methodsAllowed ...methodTyp) http.HandlerFunc { + if mx.methodNotAllowedHandler != nil { + return mx.methodNotAllowedHandler + } + return methodNotAllowedHandler(methodsAllowed...) +} + +// handle registers a http.Handler in the routing tree for a particular http method +// and routing pattern. +func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *node { + if len(pattern) == 0 || pattern[0] != '/' { + panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) + } + + // Build the computed routing handler for this routing pattern. + if !mx.inline && mx.handler == nil { + mx.updateRouteHandler() + } + + // Build endpoint handler with inline middlewares for the route + var h http.Handler + if mx.inline { + mx.handler = http.HandlerFunc(mx.routeHTTP) + h = Chain(mx.middlewares...).Handler(handler) + } else { + h = handler + } + + // Add the endpoint to the tree and return the node + return mx.tree.InsertRoute(method, pattern, h) +} + +// routeHTTP routes a http.Request through the Mux routing tree to serve +// the matching handler for a particular http method. +func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { + // Grab the route context object + rctx := r.Context().Value(RouteCtxKey).(*Context) + + // The request routing path + routePath := rctx.RoutePath + + // If RoutePath was set (either by ServeHTTP for the root router, or by + // a mount handler for a sub-router), check if r.URL.Path has been + // rewritten by middleware. If it has, synchronize RoutePath with the + // updated r.URL.Path so that subsequent routing and parameter extraction + // operate on the correct path. + // + // This fixes the bug where a middleware rewrites r.URL.Path but the + // routing context's RoutePath still holds the original (stale) path, + // causing URL parameters to be lost or routes to be mismatched. + if routePath != "" && rctx.routePathSource != "" && r.URL.Path != rctx.routePathSource { + // r.URL.Path was rewritten by middleware; recompute RoutePath + if rctx.mountPrefix != "" && strings.HasPrefix(r.URL.Path, rctx.mountPrefix) { + // Sub-router: strip the mount prefix to get the sub-path + routePath = r.URL.Path[len(rctx.mountPrefix)-1:] // keep leading "/" + if routePath == "" { + routePath = "/" + } + } else { + // Root router or mount prefix was stripped by the rewrite: + // use the full r.URL.Path + routePath = r.URL.Path + } + rctx.RoutePath = routePath + rctx.routePathSource = r.URL.Path + } + + if routePath == "" { + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } else { + routePath = r.URL.Path + } + if routePath == "" { + routePath = "/" + } + // Cache the routing path in the context so that subsequent + // routing steps (e.g., sub-routers) can reference it. + rctx.RoutePath = routePath + rctx.routePathSource = r.URL.Path + } + + // Check if method is supported by chi + if rctx.RouteMethod == "" { + rctx.RouteMethod = r.Method + } + method, ok := methodMap[rctx.RouteMethod] + if !ok { + mx.MethodNotAllowedHandler().ServeHTTP(w, r) + return + } + + // Find the route + if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { + // Set http.Request path values from our request context + for i, key := range rctx.URLParams.Keys { + value := rctx.URLParams.Values[i] + r.SetPathValue(key, value) + } + r.Pattern = rctx.RoutePattern() + + h.ServeHTTP(w, r) + return + } + if rctx.methodNotAllowed { + mx.MethodNotAllowedHandler(rctx.methodsAllowed...).ServeHTTP(w, r) + } else { + mx.NotFoundHandler().ServeHTTP(w, r) + } +} + +func (mx *Mux) nextRoutePath(rctx *Context) string { + routePath := "/" + nx := len(rctx.routeParams.Keys) - 1 // index of last param in list + if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { + routePath = "/" + rctx.routeParams.Values[nx] + } + return routePath +} + +// updateSubRoutes updates the subroutes of the mux +func (mx *Mux) updateSubRoutes(fn func(subMux *Mux)) { + mx.tree.updateSubRoutes(fn) +} + +// updateRouteHandler builds the computed mux handler +func (mx *Mux) updateRouteHandler() { + mx.handler = Chain(mx.middlewares...).Handler(http.HandlerFunc(mx.routeHTTP)) +} \ No newline at end of file diff --git a/mux_test.go b/mux_test.go new file mode 100644 index 0000000..33c5747 --- /dev/null +++ b/mux_test.go @@ -0,0 +1,318 @@ +package chi + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// indexOfPathSegment returns the byte index of the start of a path segment +// in the URL path, or -1 if not found. For example, searching for "legacy" +// in "/api/v1/legacy/42" returns 8 (the index of "legacy"). +func indexOfPathSegment(path, segment string) int { + parts := strings.Split(path, "/") + offset := 0 + for _, part := range parts { + if part == segment { + return offset + } + // Account for the "/" separator + offset += len(part) + 1 + } + return -1 +} + +// TestMiddlewarePathRewriteURLParams tests that when a middleware rewrites +// r.URL.Path, subsequent routing correctly matches the rewritten path and +// extracts URL parameters from it. +// +// This is the primary test case from issue #1. +func TestMiddlewarePathRewriteURLParams(t *testing.T) { + r := NewRouter() + + // Middleware that rewrites the path + 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 := 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) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestMiddlewarePathRewriteWithSubRouter tests that path rewriting works +// correctly with nested sub-routers mounted via Mount(). +// +// This test demonstrates the core bug: when a middleware on the sub-router +// rewrites r.URL.Path, the RouteContext's RoutePath (which was set by the +// mount handler based on the original path) becomes stale. The sub-router's +// routeHTTP then tries to match against the stale RoutePath instead of the +// updated r.URL.Path, causing URL parameters to be lost. +func TestMiddlewarePathRewriteWithSubRouter(t *testing.T) { + r := NewRouter() + + // Sub-router with a middleware that rewrites the path + subRouter := NewRouter() + subRouter.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Rewrite /api/v1/legacy/{id} -> /api/v1/users/{id} + // (rewriting the full r.URL.Path, as a middleware would) + if idx := indexOfPathSegment(req.URL.Path, "legacy"); idx >= 0 { + req.URL.Path = req.URL.Path[:idx] + "users" + req.URL.Path[idx+6:] + } + next.ServeHTTP(w, req) + }) + }) + subRouter.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) { + id := URLParam(req, "id") + if id != "42" { + t.Errorf("expected URL param 'id' to be '42', got '%s'", id) + } + w.Write([]byte("ok")) + }) + + // Mount the sub-router at /api/v1 + r.Mount("/api/v1", subRouter) + + ts := httptest.NewServer(r) + defer ts.Close() + + // Request: /api/v1/legacy/42 + // Parent router matches /api/v1/* and dispatches to sub-router + // Sub-router middleware rewrites /api/v1/legacy/42 -> /api/v1/users/42 + // Sub-router should match /users/{id} with id=42 + res, err := http.Get(ts.URL + "/api/v1/legacy/42") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestMiddlewarePathRewriteWildcard tests that path rewriting works with +// wildcard routes. +func TestMiddlewarePathRewriteWildcard(t *testing.T) { + r := NewRouter() + + // Middleware that rewrites the path + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/old" { + req.URL.Path = "/new" + } + next.ServeHTTP(w, req) + }) + }) + + r.Get("/*", func(w http.ResponseWriter, req *http.Request) { + w.Write([]byte("wildcard")) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + res, err := http.Get(ts.URL + "/old") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestNoMiddlewareRewriteBackwardCompat tests that existing middlewares that +// do NOT modify r.URL.Path continue to work correctly. +func TestNoMiddlewareRewriteBackwardCompat(t *testing.T) { + r := NewRouter() + + // Middleware that does NOT rewrite the path + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Just pass through, no path modification + next.ServeHTTP(w, req) + }) + }) + + r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) { + id := URLParam(req, "id") + if id != "99" { + t.Errorf("expected URL param 'id' to be '99', got '%s'", id) + } + w.Write([]byte("ok")) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + res, err := http.Get(ts.URL + "/users/99") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestBasicRoutingNoMiddleware tests basic routing without any middleware +// to ensure backward compatibility. +func TestBasicRoutingNoMiddleware(t *testing.T) { + r := NewRouter() + + r.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) { + id := URLParam(req, "id") + if id != "5" { + t.Errorf("expected URL param 'id' to be '5', got '%s'", id) + } + w.Write([]byte("ok")) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + res, err := http.Get(ts.URL + "/users/5") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestMiddlewarePathRewriteNestedParams tests nested routing with path +// rewrite and multiple parameters. +func TestMiddlewarePathRewriteNestedParams(t *testing.T) { + r := NewRouter() + + // Middleware that rewrites /v1/foo/bar/baz to /foo/bar/baz + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if len(req.URL.Path) > 4 && req.URL.Path[:4] == "/v1/" { + req.URL.Path = req.URL.Path[3:] // strip "/v1" -> keep leading "/" + } + next.ServeHTTP(w, req) + }) + }) + + r.Get("/users/{id}/posts/{postId}", func(w http.ResponseWriter, req *http.Request) { + id := URLParam(req, "id") + postId := URLParam(req, "postId") + if id != "7" { + t.Errorf("expected URL param 'id' to be '7', got '%s'", id) + } + if postId != "13" { + t.Errorf("expected URL param 'postId' to be '13', got '%s'", postId) + } + w.Write([]byte("ok")) + }) + + ts := httptest.NewServer(r) + defer ts.Close() + + res, err := http.Get(ts.URL + "/v1/users/7/posts/13") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestMiddlewarePathRewriteMountedSubRouter tests the exact scenario from +// the issue: a middleware on a mounted sub-router rewrites r.URL.Path, and +// the sub-router must correctly match and extract URL params from the +// rewritten path. +// +// In this test, the middleware on the sub-router rewrites the path from +// /legacy/{id} to /users/{id}. The sub-router must see the rewritten path, +// not the original RoutePath set by the mount handler. +func TestMiddlewarePathRewriteMountedSubRouter(t *testing.T) { + r := NewRouter() + + // Sub-router with a middleware that strips a prefix + subRouter := NewRouter() + subRouter.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Rewrite /mount/prefix/api/users/100 -> /mount/api/users/100 + // (rewriting the full r.URL.Path, as a middleware would) + if idx := indexOfPathSegment(req.URL.Path, "prefix"); idx >= 0 { + // Remove the "/prefix" segment + req.URL.Path = req.URL.Path[:idx-1] + req.URL.Path[idx+6:] + } + next.ServeHTTP(w, req) + }) + }) + subRouter.Get("/api/users/{id}", func(w http.ResponseWriter, req *http.Request) { + id := URLParam(req, "id") + if id != "100" { + t.Errorf("expected URL param 'id' to be '100', got '%s'", id) + } + w.Write([]byte("ok")) + }) + + // Mount the sub-router at /mount + r.Mount("/mount", subRouter) + + ts := httptest.NewServer(r) + defer ts.Close() + + // Request: /mount/prefix/api/users/100 + // Parent router matches /mount/* and dispatches to sub-router + // Sub-router middleware rewrites /mount/prefix/api/users/100 -> /mount/api/users/100 + // Sub-router should match /api/users/{id} with id=100 + res, err := http.Get(ts.URL + "/mount/prefix/api/users/100") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} + +// TestSubRouterNoMiddlewareBackwardCompat tests that mounted sub-routers +// without path-rewriting middleware continue to work correctly. +func TestSubRouterNoMiddlewareBackwardCompat(t *testing.T) { + r := NewRouter() + + subRouter := NewRouter() + subRouter.Get("/users/{id}", func(w http.ResponseWriter, req *http.Request) { + id := URLParam(req, "id") + if id != "50" { + t.Errorf("expected URL param 'id' to be '50', got '%s'", id) + } + w.Write([]byte("ok")) + }) + r.Mount("/api", subRouter) + + ts := httptest.NewServer(r) + defer ts.Close() + + res, err := http.Get(ts.URL + "/api/users/50") + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("expected status 200, got %d", res.StatusCode) + } +} \ No newline at end of file diff --git a/mux_utils.go b/mux_utils.go new file mode 100644 index 0000000..7087a4d --- /dev/null +++ b/mux_utils.go @@ -0,0 +1,118 @@ +package chi + +import ( + "net/http" +) + +// Router interface for chi's Mux +type Router interface { + http.Handler + + // Use appends a middleware handler to the Mux middleware stack. + Use(middlewares ...func(http.Handler) http.Handler) + + // Handle adds the route `pattern` that matches any http method to + // execute the `handler` http.Handler. + Handle(pattern string, handler http.Handler) + + // HandleFunc adds the route `pattern` that matches any http method to + // execute the `handlerFn` http.HandlerFunc. + HandleFunc(pattern string, handlerFn http.HandlerFunc) + + // Method adds the route `pattern` that matches `method` http method to + // execute the `handler` http.Handler. + Method(method, pattern string, handler http.Handler) + + // MethodFunc adds the route `pattern` that matches `method` http method to + // execute the `handlerFn` http.HandlerFunc. + MethodFunc(method, pattern string, handlerFn http.HandlerFunc) + + // Connect adds the route `pattern` that matches a CONNECT http method to + // execute the `handlerFn` http.HandlerFunc. + Connect(pattern string, handlerFn http.HandlerFunc) + + // Delete adds the route `pattern` that matches a DELETE http method to + // execute the `handlerFn` http.HandlerFunc. + Delete(pattern string, handlerFn http.HandlerFunc) + + // Get adds the route `pattern` that matches a GET http method to + // execute the `handlerFn` http.HandlerFunc. + Get(pattern string, handlerFn http.HandlerFunc) + + // Head adds the route `pattern` that matches a HEAD http method to + // execute the `handlerFn` http.HandlerFunc. + Head(pattern string, handlerFn http.HandlerFunc) + + // Options adds the route `pattern` that matches an OPTIONS http method to + // execute the `handlerFn` http.HandlerFunc. + Options(pattern string, handlerFn http.HandlerFunc) + + // Patch adds the route `pattern` that matches a PATCH http method to + // execute the `handlerFn` http.HandlerFunc. + Patch(pattern string, handlerFn http.HandlerFunc) + + // Post adds the route `pattern` that matches a POST http method to + // execute the `handlerFn` http.HandlerFunc. + Post(pattern string, handlerFn http.HandlerFunc) + + // Put adds the route `pattern` that matches a PUT http method to + // execute the `handlerFn` http.HandlerFunc. + Put(pattern string, handlerFn http.HandlerFunc) + + // Trace adds the route `pattern` that matches a TRACE http method to + // execute the `handlerFn` http.HandlerFunc. + Trace(pattern string, handlerFn http.HandlerFunc) + + // NotFound sets a custom http.HandlerFunc for routing paths that could + // not be found. + NotFound(handlerFn http.HandlerFunc) + + // MethodNotAllowed sets a custom http.HandlerFunc for routing paths where + // the method is unresolved. + MethodNotAllowed(handlerFn http.HandlerFunc) + + // With adds inline middlewares for an endpoint handler. + With(middlewares ...func(http.Handler) http.Handler) Router + + // Group creates a new inline-Mux with a copy of middleware stack. + Group(fn func(r Router)) Router + + // Route creates a new Mux and mounts it along the `pattern` as a subrouter. + Route(pattern string, fn func(r Router)) Router + + // Mount attaches another http.Handler or chi Router as a subrouter along a routing + // path. + Mount(pattern string, handler http.Handler) + + // Routes returns a slice of routing information from the tree. + Routes() []Route + + // Middlewares returns a slice of middleware handler functions. + Middlewares() Middlewares + + // Match searches the routing tree for a handler that matches the method/path. + Match(rctx *Context, method, path string) bool + + // Find searches the routing tree for the pattern that matches + // the method/path. + Find(rctx *Context, method, path string) string +} + +// Routes interface for routers that can expose their routes +type Routes interface { + Routes() []Route +} + +// Route information +type Route struct { + Pattern string + Handler http.Handler +} + +// WalkFunc is a function that walks the routing tree +type WalkFunc func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error + +// NewRouter returns a new Mux object that implements the Router interface. +func NewRouter() *Mux { + return NewMux() +} \ No newline at end of file diff --git a/tree.go b/tree.go new file mode 100644 index 0000000..7f34e39 --- /dev/null +++ b/tree.go @@ -0,0 +1,313 @@ +package chi + +import ( + "net/http" + "strings" +) + +// node is a radix trie node for the routing tree. +type node struct { + // The routing pattern (e.g., "/users/{id}") + pattern string + + // The raw tail of the pattern (the part after the parent's pattern) + tail string + + // Parent node + parent *node + + // Child nodes + children []*node + + // Endpoint handlers indexed by method + endpoints endpoints + + // Subroutes for mounted sub-routers + subroutes Router + + // Whether this node is a wildcard (matches remaining path) + isWildcard bool + + // Whether this node is a parameter node (contains {param}) + isParam bool + + // The parameter name (if isParam) + paramName string + + // The static prefix (for non-param nodes) + prefix string +} + +// endpoints stores the handler and pattern for each method +type endpoints map[methodTyp]endpoint + +type endpoint struct { + pattern string + handler http.Handler +} + +// findPattern checks if a pattern exists in the tree +func (n *node) findPattern(pattern string) bool { + return n.findPatternRecursive(pattern) +} + +func (n *node) findPatternRecursive(pattern string) bool { + if n.pattern == pattern { + return true + } + for _, child := range n.children { + if child.findPatternRecursive(pattern) { + return true + } + } + return false +} + +// updateSubRoutes traverses the tree and applies fn to each mux +func (n *node) updateSubRoutes(fn func(subMux *Mux)) { + for _, child := range n.children { + child.updateSubRoutes(fn) + } +} + +// routes returns all routes in the tree +func (n *node) routes() []Route { + var routes []Route + for _, child := range n.children { + for m, e := range child.endpoints { + if m&mSTUB != 0 { + continue + } + routes = append(routes, Route{Pattern: e.pattern, Handler: e.handler}) + } + routes = append(routes, child.routes()...) + } + return routes +} + +// InsertRoute inserts a route into the tree +func (n *node) InsertRoute(method methodTyp, pattern string, handler http.Handler) *node { + // Remove trailing slash for consistency (except root) + if len(pattern) > 1 && pattern[len(pattern)-1] == '/' { + pattern = strings.TrimSuffix(pattern, "/") + } + + // Check if this pattern already exists as a child + for _, child := range n.children { + if child.pattern == pattern { + child.setEndpoint(method, pattern, handler) + return child + } + } + + // Create a new node + child := &node{ + pattern: pattern, + parent: n, + endpoints: endpoints{}, + } + + // Parse the pattern to determine node type + child.parsePattern(pattern) + + // Determine the tail (the part unique to this node relative to parent) + child.tail = pattern + if n.pattern != "" && strings.HasPrefix(pattern, n.pattern) { + child.tail = pattern[len(n.pattern):] + } + + n.children = append(n.children, child) + child.setEndpoint(method, pattern, handler) + return child +} + +// parsePattern analyzes a pattern and sets node properties +func (n *node) parsePattern(pattern string) { + // Check for wildcard + if strings.HasSuffix(pattern, "/*") || strings.HasSuffix(pattern, "*") { + n.isWildcard = true + } + + // Check for parameter in the last segment + parts := strings.Split(pattern, "/") + lastPart := parts[len(parts)-1] + if len(lastPart) > 0 && lastPart[0] == '{' && lastPart[len(lastPart)-1] == '}' { + n.isParam = true + n.paramName = lastPart[1 : len(lastPart)-1] + } + + // Set prefix for static matching + n.prefix = pattern +} + +// setEndpoint sets the handler for a given method +func (n *node) setEndpoint(method methodTyp, pattern string, handler http.Handler) { + if n.endpoints == nil { + n.endpoints = endpoints{} + } + n.endpoints[method] = endpoint{pattern: pattern, handler: handler} +} + +// FindRoute searches the tree for a matching route and populates the context +// with URL parameters. +func (n *node) FindRoute(rctx *Context, method methodTyp, path string) (*node, endpoint, http.Handler) { + // Try to match against this node's children + for _, child := range n.children { + if matched, node, h := child.match(rctx, method, path); matched { + return node, endpoint{}, h + } + } + return nil, endpoint{}, nil +} + +// match attempts to match a path against this node +func (n *node) match(rctx *Context, method methodTyp, path string) (bool, *node, http.Handler) { + // Try exact match first + if n.pattern == path { + // Check if there's a handler for this method + if e, ok := n.endpoints[method]; ok { + return true, n, e.handler + } + // Check for mALL (Handle without method) + if e, ok := n.endpoints[mALL]; ok { + return true, n, e.handler + } + // Check for stub (mount points) + if e, ok := n.endpoints[mALL|mSTUB]; ok { + return true, n, e.handler + } + // Method not allowed - check what methods are registered + if len(n.endpoints) > 0 { + rctx.methodNotAllowed = true + for m := range n.endpoints { + if m&mSTUB == 0 { + rctx.methodsAllowed = append(rctx.methodsAllowed, m) + } + } + } + return false, nil, nil + } + + // Try wildcard match (pattern ends with /*) + if strings.HasSuffix(n.pattern, "/*") { + prefix := strings.TrimSuffix(n.pattern, "/*") + if path == prefix || strings.HasPrefix(path, prefix+"/") || (prefix == "" && strings.HasPrefix(path, "/")) { + // Extract the wildcard value + wildcardValue := "" + if len(path) > len(prefix) { + wildcardValue = path[len(prefix)+1:] + } + rctx.routeParams.Add("*", wildcardValue) + rctx.URLParams.Add("*", wildcardValue) + rctx.routePattern = n.pattern + + // Check for handler — try specific method first, then mALL, then stub + if e, ok := n.endpoints[method]; ok { + return true, n, e.handler + } + if e, ok := n.endpoints[mALL]; ok { + return true, n, e.handler + } + if e, ok := n.endpoints[mALL|mSTUB]; ok { + return true, n, e.handler + } + } + } + + // Try pattern match with parameters + if n.isParam { + // Get the parent prefix + parentPrefix := "" + if n.parent != nil && n.parent.pattern != "" { + parentPrefix = n.parent.pattern + } + + // The prefix before the param segment + paramPrefix := parentPrefix + "/" + if !strings.HasPrefix(path, paramPrefix) { + return false, nil, nil + } + + // Extract the param value (everything up to the next / or end) + remaining := path[len(paramPrefix):] + paramValue := remaining + if idx := strings.Index(remaining, "/"); idx >= 0 { + paramValue = remaining[:idx] + } + + // Check if the full path matches + expectedPath := paramPrefix + paramValue + if path == expectedPath { + rctx.routeParams.Add(n.paramName, paramValue) + rctx.URLParams.Add(n.paramName, paramValue) + rctx.routePattern = n.pattern + + if e, ok := n.endpoints[method]; ok { + return true, n, e.handler + } + if e, ok := n.endpoints[mALL]; ok { + return true, n, e.handler + } + } + } + + // Try matching with parameters in the pattern + matched, paramValues := matchPattern(n.pattern, path) + if matched { + // Extract parameter names from pattern + paramNames := extractParamNames(n.pattern) + for i, name := range paramNames { + if i < len(paramValues) { + rctx.routeParams.Add(name, paramValues[i]) + rctx.URLParams.Add(name, paramValues[i]) + } + } + rctx.routePattern = n.pattern + + if e, ok := n.endpoints[method]; ok { + return true, n, e.handler + } + if e, ok := n.endpoints[mALL]; ok { + return true, n, e.handler + } + if e, ok := n.endpoints[mALL|mSTUB]; ok { + return true, n, e.handler + } + } + + return false, nil, nil +} + +// matchPattern checks if a path matches a pattern and extracts parameter values +func matchPattern(pattern, path string) (bool, []string) { + patternParts := strings.Split(pattern, "/") + pathParts := strings.Split(path, "/") + + if len(patternParts) != len(pathParts) { + return false, nil + } + + var params []string + for i, pp := range patternParts { + lp := pathParts[i] + if len(pp) > 0 && pp[0] == '{' && pp[len(pp)-1] == '}' { + // This is a parameter + params = append(params, lp) + } else if pp != lp { + return false, nil + } + } + return true, params +} + +// extractParamNames extracts parameter names from a pattern like "/users/{id}" +func extractParamNames(pattern string) []string { + var names []string + parts := strings.Split(pattern, "/") + for _, part := range parts { + if len(part) > 0 && part[0] == '{' && part[len(part)-1] == '}' { + names = append(names, part[1:len(part)-1]) + } + } + return names +} \ No newline at end of file