Skip to content

Commit 73c4ecd

Browse files
authored
http: set r.Pattern on dispatched requests for automatic route tagging (#3897)
The default HTTP muxer now populates r.Pattern with "METHOD /path" on every matched request, following the Go 1.22+ convention established by http.ServeMux. This enables observability middleware such as otelhttp (v0.65.0+) to automatically tag spans and metrics with the matched route without any per-handler wrapping. The pattern is captured before chi's internal wildcard rewriting, so r.Pattern always reflects the original Goa route (e.g. GET /users/{id}, GET /files/{*path}). This replaces the need for the otel plugin in goa.design/plugins which has been deprecated (goadesign/plugins#242). Fixes #3896
1 parent 3d421fe commit 73c4ecd

File tree

2 files changed

+58
-1
lines changed

2 files changed

+58
-1
lines changed

http/mux.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ func NewMuxer() ResolverMuxer {
9494
var wildPath = regexp.MustCompile(`/{\*([a-zA-Z0-9_]+)}`)
9595

9696
// Handle registers the handler function for the given method and pattern.
97+
// It sets r.Pattern on every matched request to "METHOD /path" (matching the
98+
// Go 1.22+ convention used by http.ServeMux), enabling observability
99+
// middleware such as otelhttp to automatically tag spans and metrics with
100+
// the matched route.
97101
func (m *mux) Handle(method, pattern string, handler http.HandlerFunc) {
98102
m.mu.Lock()
99103
defer m.mu.Unlock()
@@ -109,14 +113,20 @@ func (m *mux) Handle(method, pattern string, handler http.HandlerFunc) {
109113
}))
110114
m.middlewares = nil
111115
}
116+
// Capture the registered pattern before wildcard rewriting so we can
117+
// populate r.Pattern for downstream consumers.
118+
reqPattern := method + " " + pattern
112119
if wildcards := wildPath.FindStringSubmatch(pattern); len(wildcards) > 0 {
113120
if len(wildcards) > 2 {
114121
panic("too many wildcards")
115122
}
116123
pattern = wildPath.ReplaceAllString(pattern, "/*")
117124
m.wildcards[method+"::"+pattern] = wildcards[1]
118125
}
119-
m.Method(method, pattern, handler)
126+
m.Method(method, pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127+
r.Pattern = reqPattern
128+
handler(w, r)
129+
}))
120130
}
121131

122132
// Vars extracts the path variables from the request context.

http/mux_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,53 @@ func TestVars(t *testing.T) {
146146
}
147147
}
148148

149+
func TestRequestPattern(t *testing.T) {
150+
cases := []struct {
151+
Name string
152+
Method string
153+
Pattern string
154+
URL string
155+
Expected string
156+
}{
157+
{
158+
Name: "simple",
159+
Method: "GET",
160+
Pattern: "/users",
161+
URL: "/users",
162+
Expected: "GET /users",
163+
},
164+
{
165+
Name: "with segment",
166+
Method: "POST",
167+
Pattern: "/users/{id}",
168+
URL: "/users/123",
169+
Expected: "POST /users/{id}",
170+
},
171+
{
172+
Name: "with wildcard",
173+
Method: "GET",
174+
Pattern: "/files/{*path}",
175+
URL: "/files/a/b/c",
176+
Expected: "GET /files/{*path}",
177+
},
178+
}
179+
180+
for _, c := range cases {
181+
t.Run(c.Name, func(t *testing.T) {
182+
var called bool
183+
mux := NewMuxer()
184+
mux.Handle(c.Method, c.Pattern, func(_ http.ResponseWriter, r *http.Request) {
185+
assert.Equal(t, c.Expected, r.Pattern)
186+
called = true
187+
})
188+
req, _ := http.NewRequest(c.Method, c.URL, nil)
189+
w := httptest.NewRecorder()
190+
mux.ServeHTTP(w, req)
191+
assert.True(t, called)
192+
})
193+
}
194+
}
195+
149196
func TestResolvePattern(t *testing.T) {
150197
cases := []struct {
151198
Name string

0 commit comments

Comments
 (0)