fix(group): force leading slash on Go route paths for contract-id parity

normalizeHttpPath (the shared contract-id normalizer) does not add a
leading slash, while ingestion's normalizeExtractedRoutePath always
does — so a literal path "x" or a slashless Group("api") prefix split
the contract id across the two strategies (`http::GET::x` vs
`http::GET::/x`). After joining and collapsing "//", ensure the result
starts with "/" so both strategies feed the same bytes into the id.
Gin also panics when registering a route path without a leading slash.

Tests: go-gin-route-groups 35, group suite 1298 (all green).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
rgb-vgx 2026-10-04 12:11:34 +07:00
parent d0bfc3ee8d
commit f4dde07ef1
2 changed files with 29 additions and 1 deletions

View file

@ -108,7 +108,13 @@ function joinRoutePath(prefix: string, relative: string): string {
// just the join — because ingestion's normalizeExtractedRoutePath collapses
// all "//" while the downstream contract-id normalizer does not: a path
// that keeps "//" would split into two contract ids across the strategies.
return joined.replace(/\/+/g, '/');
const collapsed = joined.replace(/\/+/g, '/');
// Force a leading "/" for the same reason: ingestion's
// normalizeExtractedRoutePath always adds one, while normalizeHttpPath (the
// shared contract-id normalizer) does not — a literal "x" or a slashless
// Group("api") prefix would emit `...::x` here and `...::/x` there. Gin
// also refuses a registration path that does not start with "/".
return collapsed.startsWith('/') ? collapsed : `/${collapsed}`;
}
/** `parent.Group("/p", mw...)` → its receiver and literal prefix; null otherwise. */

View file

@ -441,6 +441,28 @@ func routes(r *gin.Engine) {
]);
});
it('forces a leading slash on slashless literals and group prefixes', () => {
// Ingestion's normalizeExtractedRoutePath always adds a leading "/" but
// normalizeHttpPath (the shared contract-id normalizer) does not — a path
// like "x" would become `http::GET::x` here and `http::GET::/x` there,
// splitting the id across the two strategies. Gin likewise panics when a
// route is registered without one.
expect(
providers(`package main
func routes(r *gin.Engine) {
r.GET("x", h.X)
g := r.Group("api")
g.GET("y", h.Y)
r.GET("", h.Root)
}
`),
).toEqual([
{ method: 'GET', path: '/x', name: 'X' },
{ method: 'GET', path: '/api/y', name: 'Y' },
{ method: 'GET', path: '/', name: 'Root' },
]);
});
it("binds echo's handler (first argument) when the file imports echo only", () => {
// echo is `GET(path, handler, middleware...)` — the handler is second,
// not last, so a middleware selector must not become the route's name.