Skip to content
Merged
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
26 changes: 23 additions & 3 deletions http/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,19 @@ type (
)

// NewMuxer returns a Muxer implementation based on a Chi router.
//
// The returned muxer sets r.Pattern (Go 1.22+) on every dispatched request
// before middlewares run. This allows observability middleware such as
// otelhttp to read the matched route from r.Pattern for span attributes and
// metrics. To take advantage of this, register otelhttp as a mux middleware
// rather than wrapping the mux externally:
//
// mux := goahttp.NewMuxer()
// mux.Use(otelhttp.NewMiddleware("service"))
func NewMuxer() ResolverMuxer {
return &mux{
Router: chi.NewRouter(),
wildcards: make(map[string]string),
middlewares: nil,
Router: chi.NewRouter(),
wildcards: make(map[string]string),
}
}

Expand Down Expand Up @@ -129,6 +137,18 @@ func (m *mux) Handle(method, pattern string, handler http.HandlerFunc) {
}))
}

// ServeHTTP resolves the matched route and sets r.Pattern before dispatching
// the request through chi's middleware chain and handler. This ensures that
// middlewares registered via Use() — such as otelhttp.NewMiddleware — can
// read r.Pattern to tag spans and metrics with the http.route attribute.
func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rctx := chi.NewRouteContext()
if m.Match(rctx, r.Method, r.URL.Path) {
r.Pattern = r.Method + " " + m.resolveWildcard(r.Method, rctx.RoutePattern())
}
m.Router.ServeHTTP(w, r)
}

// Vars extracts the path variables from the request context.
func (m *mux) Vars(r *http.Request) map[string]string {
ctx := m.ensureContext(r)
Expand Down
57 changes: 57 additions & 0 deletions http/mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,63 @@ func TestRequestPattern(t *testing.T) {
}
}

// TestRequestPatternInMiddleware verifies that r.Pattern is set by the
// built-in middleware before user-registered middlewares run. This is
// critical for observability middleware (e.g., otelhttp) that reads
// r.Pattern to set the http.route span attribute at span-start time.
func TestRequestPatternInMiddleware(t *testing.T) {
cases := []struct {
Name string
Method string
Pattern string
URL string
Expected string
}{
{
Name: "simple",
Method: "GET",
Pattern: "/users",
URL: "/users",
Expected: "GET /users",
},
{
Name: "with segment",
Method: "POST",
Pattern: "/users/{id}",
URL: "/users/123",
Expected: "POST /users/{id}",
},
{
Name: "with wildcard",
Method: "GET",
Pattern: "/files/{*path}",
URL: "/files/a/b/c",
Expected: "GET /files/{*path}",
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
var middlewareCalled bool
mux := NewMuxer()
// Register a user middleware that reads r.Pattern —
// simulating otelhttp.NewMiddleware.
mux.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, c.Expected, r.Pattern)
middlewareCalled = true
next.ServeHTTP(w, r)
})
})
mux.Handle(c.Method, c.Pattern, func(_ http.ResponseWriter, _ *http.Request) {})
req, _ := http.NewRequest(c.Method, c.URL, nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
assert.True(t, middlewareCalled)
})
}
}

func TestResolvePattern(t *testing.T) {
cases := []struct {
Name string
Expand Down
Loading