Skip to content

Commit 15bb233

Browse files
authored
http: set r.Pattern in ServeHTTP before middleware dispatch (#3898)
The previous commit set r.Pattern inside the handler wrapper, which runs after otelhttp reads it in RequestTraceAttrs. This meant the http.route span attribute was never populated. Override ServeHTTP to pre-resolve the route via chi.Match and set r.Pattern before chi's middleware chain runs. This ensures that observability middleware registered via mux.Use() — such as otelhttp.NewMiddleware — can read r.Pattern at span-start time to set http.route on both spans and metrics. Add TestRequestPatternInMiddleware to verify r.Pattern is visible to middlewares registered via Use().
1 parent dffd5d0 commit 15bb233

2 files changed

Lines changed: 80 additions & 3 deletions

File tree

http/mux.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,19 @@ type (
8282
)
8383

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

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

140+
// ServeHTTP resolves the matched route and sets r.Pattern before dispatching
141+
// the request through chi's middleware chain and handler. This ensures that
142+
// middlewares registered via Use() — such as otelhttp.NewMiddleware — can
143+
// read r.Pattern to tag spans and metrics with the http.route attribute.
144+
func (m *mux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
145+
rctx := chi.NewRouteContext()
146+
if m.Match(rctx, r.Method, r.URL.Path) {
147+
r.Pattern = r.Method + " " + m.resolveWildcard(r.Method, rctx.RoutePattern())
148+
}
149+
m.Router.ServeHTTP(w, r)
150+
}
151+
132152
// Vars extracts the path variables from the request context.
133153
func (m *mux) Vars(r *http.Request) map[string]string {
134154
ctx := m.ensureContext(r)

http/mux_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,63 @@ func TestRequestPattern(t *testing.T) {
193193
}
194194
}
195195

196+
// TestRequestPatternInMiddleware verifies that r.Pattern is set by the
197+
// built-in middleware before user-registered middlewares run. This is
198+
// critical for observability middleware (e.g., otelhttp) that reads
199+
// r.Pattern to set the http.route span attribute at span-start time.
200+
func TestRequestPatternInMiddleware(t *testing.T) {
201+
cases := []struct {
202+
Name string
203+
Method string
204+
Pattern string
205+
URL string
206+
Expected string
207+
}{
208+
{
209+
Name: "simple",
210+
Method: "GET",
211+
Pattern: "/users",
212+
URL: "/users",
213+
Expected: "GET /users",
214+
},
215+
{
216+
Name: "with segment",
217+
Method: "POST",
218+
Pattern: "/users/{id}",
219+
URL: "/users/123",
220+
Expected: "POST /users/{id}",
221+
},
222+
{
223+
Name: "with wildcard",
224+
Method: "GET",
225+
Pattern: "/files/{*path}",
226+
URL: "/files/a/b/c",
227+
Expected: "GET /files/{*path}",
228+
},
229+
}
230+
231+
for _, c := range cases {
232+
t.Run(c.Name, func(t *testing.T) {
233+
var middlewareCalled bool
234+
mux := NewMuxer()
235+
// Register a user middleware that reads r.Pattern —
236+
// simulating otelhttp.NewMiddleware.
237+
mux.Use(func(next http.Handler) http.Handler {
238+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
239+
assert.Equal(t, c.Expected, r.Pattern)
240+
middlewareCalled = true
241+
next.ServeHTTP(w, r)
242+
})
243+
})
244+
mux.Handle(c.Method, c.Pattern, func(_ http.ResponseWriter, _ *http.Request) {})
245+
req, _ := http.NewRequest(c.Method, c.URL, nil)
246+
w := httptest.NewRecorder()
247+
mux.ServeHTTP(w, req)
248+
assert.True(t, middlewareCalled)
249+
})
250+
}
251+
}
252+
196253
func TestResolvePattern(t *testing.T) {
197254
cases := []struct {
198255
Name string

0 commit comments

Comments
 (0)