Go / Gin

Gin Route Wildcard Param Collision With Static Subroutes

Gin's router panics at startup (not at request time) when it detects a conflict between a wildcard parameter route and a static route sharing an ambiguous path segment — this is intentional, strict validation preventing genuinely ambiguous routing configurations from ever being deployed in the first place.

The Problem

Starting a Gin application fails immediately with a panic during route registration, before the server even begins listening:

panic: 'users' in new path '/users' conflicts with existing wildcard ':id' in existing prefix '/:id'

goroutine 1 [running]:
github.com/gin-gonic/gin.(*node).insertChild(...)

The application never actually starts, since this panic happens during the initial route tree construction, before any request handling begins.

Why It Happens

Gin uses a radix tree for route matching, and it enforces a specific rule to avoid genuinely ambiguous routing: a wildcard parameter segment (like :id) and a static segment (like a literal users) cannot coexist at the same position in the tree under the same parent path, since Gin can't determine which one should take priority for a matching request in a way that's both fast and unambiguous. Common ways this conflict is introduced:

  • Registering a parameterized route like /:id and later, separately, registering a static route like /users at the same level in the path hierarchy, without realizing both are competing for the same position in the tree
  • Routes added incrementally over time by different team members or in different files, where the resulting conflict isn't obvious from looking at any single route registration in isolation
  • A route intended to be more specific, like /users/:id, being registered in a way that conflicts with a different, unrelated wildcard route also touching the same path segment

The Fix

Restructure your routes so that static and parameterized segments don't collide at the same tree position. The most common and robust fix is nesting more specific static routes under a common, unambiguous prefix, separate from where the wildcard segment is expected:

// Conflicting version:
router.GET("/:id", getResourceById)
router.GET("/users", listUsers)  // panics - conflicts with :id above

// Fixed version - use route groups to clearly separate static and dynamic segments
api := router.Group("/api")
api.GET("/users", listUsers)
api.GET("/resources/:id", getResourceById)

If you genuinely need both a static path and a parameterized one at what seems like the same logical level, add an explicit, distinguishing static prefix to disambiguate them structurally, rather than relying on Gin to somehow infer intent from otherwise conflicting paths:

router.GET("/users/:id", getUserById)
router.GET("/users/search", searchUsers)  // this specific ordering and structure works,
                                            // since "search" and ":id" are more carefully scoped

Note that even this pattern can still conflict depending on exact registration order and Gin version behavior around static-vs-wildcard priority at the same segment — testing route registration explicitly during development (simply starting the application and confirming no panic occurs) is the most reliable way to confirm a given route structure is actually valid, rather than assuming based on how it looks.

If you're building the route structure dynamically (registering routes in a loop, or based on configuration), add explicit logging or validation before registration to catch potential conflicts with a clearer error message than Gin's own panic provides, especially useful in larger applications where the exact source of a conflicting route isn't immediately obvious from the panic message alone:

func safeRegisterRoute(router *gin.Engine, method, path string, handler gin.HandlerFunc) {
    defer func() {
        if r := recover(); r != nil {
            log.Fatalf("Route registration failed for %s %s: %v", method, path, r)
        }
    }()
    router.Handle(method, path, handler)
}

This doesn't prevent the conflict, but converts Gin's sometimes terse panic message into a clearer, more specifically labeled failure pointing at exactly which route registration attempt caused the problem, which is genuinely useful in a codebase with many routes spread across multiple files.

Still Not Working?

If restructuring routes into groups still results in conflicts, review Gin's specific rules more carefully for the exact version you're using, since routing conflict detection has evolved somewhat across Gin versions, and a pattern that's valid in one version might be flagged as conflicting in another. Checking the release notes or routing documentation for your specific installed Gin version, rather than assuming behavior based on general familiarity with an earlier version, can clarify whether a specific route structure you're attempting is genuinely supported in your current setup.