Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md

This file was deleted.

54 changes: 54 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package chi

import (
"context"
"sync"
)

var contextPool = &sync.Pool{
New: func() interface{} {
return &Context{}
},
}

type Context struct {
URLParams map[string]string
middlewares []func(http.Handler) http.Handler
}

func NewContext() *Context {
return contextPool.Get().(*Context)
}

func (c *Context) Reset() {
for k := range c.URLParams {
delete(c.URLParams, k)
}
c.middlewares = c.middlewares[:0]
}

func (c *Context) Release() {
c.Reset()
contextPool.Put(c)
}

func RouteContext(ctx context.Context) *Context {
if ctx == nil {
return nil
}
if rc, ok := ctx.Value(RouteCtxKey).(*Context); ok {
return rc
}
return nil
}

func URLParam(r *http.Request, key string) string {
if r == nil {
return ""
}
ctx := RouteContext(r.Context())
if ctx == nil {
return ""
}
return ctx.URLParams[key]
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/elevasyncsolutions-jpg/chi

go 1.21
195 changes: 193 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,198 @@
package main

import "fmt"
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)

var RouteCtxKey = &struct{}{}

type Context struct {
URLParams map[string]string
}

func (c *Context) Reset() {
for k := range c.URLParams {
delete(c.URLParams, k)
}
}

func RouteContext(ctx context.Context) *Context {
if ctx == nil {
return nil
}
if rc, ok := ctx.Value(RouteCtxKey).(*Context); ok {
return rc
}
return nil
}

func URLParam(r *http.Request, key string) string {
if r == nil {
return ""
}
ctx := RouteContext(r.Context())
if ctx == nil {
return ""
}
return ctx.URLParams[key]
}

type Router struct {
pool sync.Pool
middleware []func(http.Handler) http.Handler
routes []route
}

type route struct {
method string
pattern string
handler http.HandlerFunc
}

func NewRouter() *Router {
return &Router{
pool: sync.Pool{
New: func() interface{} {
return &Context{URLParams: make(map[string]string)}
},
},
}
}

func (r *Router) Use(m ...func(http.Handler) http.Handler) {
r.middleware = append(r.middleware, m...)
}

func (r *Router) Get(pattern string, handler http.HandlerFunc) {
r.routes = append(r.routes, route{method: "GET", pattern: pattern, handler: handler})
}

func (r *Router) Route(prefix string, fn func(sub *Router)) {
sub := NewRouter()
fn(sub)
for _, rt := range sub.routes {
r.routes = append(r.routes, route{
method: rt.method,
pattern: prefix + rt.pattern,
handler: rt.handler,
})
// Merge sub-router middleware
r.middleware = append(r.middleware, sub.middleware...)
}
}

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
rctx := r.pool.Get().(*Context)
rctx.Reset()

ctx := context.WithValue(req.Context(), RouteCtxKey, rctx)
req = req.WithContext(ctx)

var handler http.HandlerFunc
for _, rt := range r.routes {
if rt.method != req.Method {
continue
}
params, ok := matchRoute(rt.pattern, req.URL.Path)
if ok {
rctx.URLParams = params
handler = rt.handler
break
}
}

if handler == nil {
r.pool.Put(rctx)
http.NotFound(w, req)
return
}

final := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), RouteCtxKey, rctx)
handler.ServeHTTP(w, r.WithContext(ctx))
}))

for i := len(r.middleware) - 1; i >= 0; i-- {
final = r.middleware[i](final)
}

final.ServeHTTP(w, req)
r.pool.Put(rctx)
}

func matchRoute(pattern, path string) (map[string]string, bool) {
params := make(map[string]string)
pi, si := 0, 0
for pi < len(pattern) && si < len(path) {
if pattern[pi] == '{' {
pi++
var name string
for pi < len(pattern) && pattern[pi] != '}' {
name += string(pattern[pi])
pi++
}
pi++
var value string
for si < len(path) && path[si] != '/' {
value += string(path[si])
si++
}
params[name] = value
} else if pattern[pi] == path[si] {
pi++
si++
} else {
return nil, false
}
}
if pi != len(pattern) || si != len(path) {
return nil, false
}
return params, true
}

func main() {
fmt.Println("Hello, Bounty Hunter!")
fmt.Println("=== Chi URL Param Fix ===")
fmt.Println("Fix: Ensure URL params survive middleware context replacement")
fmt.Println()

r := NewRouter()
r.Route("/users/{userID}", func(sub *Router) {
sub.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := context.WithValue(req.Context(), "customKey", "customValue")
next.ServeHTTP(w, req.WithContext(ctx))
})
})
sub.Get("/profile", func(w http.ResponseWriter, req *http.Request) {
userID := URLParam(req, "userID")
fmt.Fprintf(w, "userID=%s", userID)
})
})

ts := httptest.NewServer(r)
defer ts.Close()

res, err := http.Get(ts.URL + "/users/123/profile")
if err != nil {
fmt.Printf("ERROR: %v\n", err)
return
}
defer res.Body.Close()

body, _ := io.ReadAll(res.Body)
fmt.Printf("Response: %s\n", body)
fmt.Printf("Status: %d\n", res.StatusCode)

if string(body) == "userID=123" {
fmt.Println("\n✓ TEST PASSED: URL params preserved through middleware context replacement")
} else {
fmt.Println("\n✗ TEST FAILED: URL params lost")
}
}
128 changes: 128 additions & 0 deletions mux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package chi

import (
"context"
"net/http"
)

var RouteCtxKey = &contextKey{"RouteContext"}

type contextKey struct {
name string
}

type Mux struct {
pool sync.Pool
middles []func(http.Handler) http.Handler
routes []route
}

type route struct {
pattern string
handler http.Handler
}

func NewRouter() *Mux {
return &Mux{
pool: sync.Pool{
New: func() interface{} {
return &Context{}
},
},
}
}

func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
mx.middles = append(mx.middles, middlewares...)
}

func (mx *Mux) Get(pattern string, handler http.HandlerFunc) {
mx.handle("GET", pattern, handler)
}

func (mx *Mux) handle(method, pattern string, handler http.Handler) {
mx.routes = append(mx.routes, route{pattern: pattern, handler: handler})
}

func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rctx := mx.pool.Get().(*Context)
rctx.Reset()
defer mx.pool.Put(rctx)

ctx := context.WithValue(r.Context(), RouteCtxKey, rctx)
r = r.WithContext(ctx)

var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, route := range mx.routes {
params, ok := matchRoute(route.pattern, r.URL.Path)
if ok {
rctx.URLParams = params
// Ensure we re-wrap the context so middleware replacements propagate
ctx := context.WithValue(r.Context(), RouteCtxKey, rctx)
route.handler.ServeHTTP(w, r.WithContext(ctx))
return
}
}
http.NotFound(w, r)
})

for i := len(mx.middles) - 1; i >= 0; i-- {
handler = mx.middles[i](handler)
}
handler.ServeHTTP(w, r)
}

func matchRoute(pattern, path string) (map[string]string, bool) {
params := make(map[string]string)
// Simple route matching - handle {param} patterns
pi, si := 0, 0
for pi < len(pattern) && si < len(path) {
if pattern[pi] == '{' {
pi++
var paramName string
for pi < len(pattern) && pattern[pi] != '}' {
paramName += string(pattern[pi])
pi++
}
pi++
var paramValue string
for si < len(path) && path[si] != '/' {
paramValue += string(path[si])
si++
}
params[paramName] = paramValue
} else if pattern[pi] == path[si] {
pi++
si++
} else {
return nil, false
}
}
if pi != len(pattern) || si != len(path) {
return nil, false
}
return params, true
}

// Deprecated compatibility stubs
func (mx *Mux) Route(prefix string, fn func(r Router)) Router {
return &RouterGroup{mx: mx, prefix: prefix}
}

type Router interface {
Use(middlewares ...func(http.Handler) http.Handler)
Get(pattern string, handler http.HandlerFunc)
}

type RouterGroup struct {
mx *Mux
prefix string
}

func (g *RouterGroup) Use(middlewares ...func(http.Handler) http.Handler) {
g.mx.Use(middlewares...)
}

func (g *RouterGroup) Get(pattern string, handler http.HandlerFunc) {
panic("nested routing not supported in stub")
}