gin-gonic/gin · error

catch-all routes are only allowed at the end of the path in

Error message

catch-all routes are only allowed at the end of the path in path '${fullPath}'

What it means

Thrown by (*node).insertChild in tree.go:345 when a catch-all wildcard ('*name') is not positioned at the very end of the path. A catch-all by definition consumes the rest of the URL, so anything after it (e.g. /files/*filepath/extra) is unreachable. The check is i+len(wildcard) != len(path), meaning the wildcard must extend to the final byte.

Source

Thrown at tree.go:345

				path = path[len(wildcard):]

				child := &node{
					priority: 1,
					fullPath: fullPath,
				}
				n.addChild(child)
				n = child
				continue
			}

			// Otherwise we're done. Insert the handle in the new leaf
			n.handlers = handlers
			return
		}

		// catchAll
		if i+len(wildcard) != len(path) {
			panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
		}

		if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
			pathSeg := ""
			if len(n.children) != 0 {
				pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
			}
			panic("catch-all wildcard '" + path +
				"' in new path '" + fullPath +
				"' conflicts with existing path segment '" + pathSeg +
				"' in existing prefix '" + n.path + pathSeg +
				"'")
		}

		// currently fixed width 1 for '/'
		i--
		if i < 0 || path[i] != '/' {
			panic("no / before catch-all in path '" + fullPath + "'")

View on GitHub (pinned to 34dac209ff)

Solutions

  1. Move the catch-all to the end of the path: /download/:version/*file or /api/health/*proxy.
  2. If you need to capture the entire tail including a logical suffix, parse it inside the handler from c.Param("file") instead of expressing it in the route.
  3. Drop the trailing slash after the wildcard if it is the cause: /assets/*path rather than /assets/*path/.

Example fix

// before
router.GET("/download/*file/version", h) // panics

// after — catch-all last
router.GET("/download/:version/*file", h)

// or — parse the suffix in the handler
router.GET("/download/*file", func(c *gin.Context) {
    parts := strings.SplitN(c.Param("file"), "/version", 2)
    // parts[0] = file path, parts[1] (if present) = remainder
})
Defensive patterns

Strategy: validation

Validate before calling

// Reject catch-all wildcards that are not at the end of the path.
func validateCatchAllAtEnd(p string) error {
    idx := strings.Index(p, "*")
    if idx == -1 {
        return nil
    }
    if idx != len(p)-1 && !strings.HasSuffix(p, "/") {
        // find end of the wildcard segment
        end := strings.IndexByte(p[idx:], '/')
        if end != -1 {
            return fmt.Errorf("catch-all in %q must be the last segment", p)
        }
    }
    return nil
}

Try / catch

func safeRegister(e *gin.Engine, method, path string, h gin.HandlerFunc) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("catch-all position invalid in %s: %v", path, r)
        }
    }()
    e.Handle(method, path, h)
    return nil
}

Prevention

When it happens

Trigger: Registering /download/*file/version, /api/*proxy/health, or /static/*css/style.css — any path where text follows the catch-all segment. Also triggered by trailing-slash confusion like /assets/*path/ (the slash after the wildcard counts as 'after').

Common situations: Designing a proxy or file-serving route and wanting to anchor something after the wildcard; refactoring a path and appending a suffix without realising the catch-all must come last.

Related errors


AI-assisted analysis of gin-gonic/gin@34dac209ff (2026-08-04). Data as JSON: /data/errors/70fa15d95c0be189.json. Report an issue: GitHub.