From 718c0f6314233a490492e3d450f14828fbac76fe Mon Sep 17 00:00:00 2001 From: delthas Date: Wed, 15 Jul 2026 13:55:26 +0200 Subject: [PATCH] middleware: set http.Request.Pattern to the matched route Since Go 1.22, http.Request.Pattern carries the route pattern that matched a request; router-agnostic observability middleware (otelhttp, Prometheus, structured logging) reads it to label spans/metrics by route template instead of raw path. net/http.ServeMux populates it. Set it from RouteInfo to the matched route (BasePath + PathPattern) so those middleware work with go-openapi's router without bespoke glue. It is set in place, mirroring ServeMux, so a handler wrapping the router observes it, and stays empty for unmatched requests. Signed-off-by: delthas --- middleware/context.go | 1 + middleware/context_pattern_test.go | 43 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 middleware/context_pattern_test.go diff --git a/middleware/context.go b/middleware/context.go index 8291f63a..12abfd48 100644 --- a/middleware/context.go +++ b/middleware/context.go @@ -442,6 +442,7 @@ func (c *Context) RouteInfo(request *http.Request) (*MatchedRoute, *http.Request if route, ok := c.LookupRoute(request); ok { rCtx = stdContext.WithValue(rCtx, ctxMatchedRoute, route) + request.Pattern = route.BasePath + route.PathPattern return route, request.WithContext(rCtx), ok } diff --git a/middleware/context_pattern_test.go b/middleware/context_pattern_test.go new file mode 100644 index 00000000..ca470dff --- /dev/null +++ b/middleware/context_pattern_test.go @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers +// SPDX-License-Identifier: Apache-2.0 + +package middleware + +import ( + "net/http" + "strings" + "testing" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/internal/testing/petstore" + "github.com/go-openapi/testify/v2/assert" + "github.com/go-openapi/testify/v2/require" +) + +func TestRouteInfoSetsRequestPattern(t *testing.T) { + spec, api := petstore.NewAPI(t) + ctx := NewContext(spec, api, nil) + ctx.router = DefaultRouter(spec, ctx.api) + + t.Run("sets Request.Pattern to the matched route", func(t *testing.T) { + request, err := runtime.JSONRequest(http.MethodGet, "/api/pets/123", nil) + require.NoError(t, err) + + mr, reqWithCtx, ok := ctx.RouteInfo(request) + require.True(t, ok) + require.NotNil(t, reqWithCtx) + + assert.Equal(t, mr.BasePath+mr.PathPattern, reqWithCtx.Pattern) + assert.True(t, strings.HasSuffix(reqWithCtx.Pattern, "/pets/{id}"), reqWithCtx.Pattern) + assert.Equal(t, reqWithCtx.Pattern, request.Pattern) + }) + + t.Run("leaves Request.Pattern empty when no route matches", func(t *testing.T) { + request, err := runtime.JSONRequest(http.MethodGet, "/api/not-a-route", nil) + require.NoError(t, err) + + _, _, ok := ctx.RouteInfo(request) + require.False(t, ok) + assert.Empty(t, request.Pattern) + }) +}