gin-gonic/gin · error

handlers are already registered for path

Error message

handlers are already registered for path '${fullPath}'

What it means

Thrown by (*node).addRoute in tree.go:243 when addRoute reaches a leaf node that already has handlers attached. Gin's radix tree stores at most one handler chain per (method, path) pair, so registering the exact same method+path twice is treated as a programmer error rather than silently overwriting. The panic message echoes the duplicated fullPath so you can locate the offending registration.

Solutions

  1. Search the codebase for the exact path string from the panic message — there will be two registrations; delete or rename one.
  2. If the duplication comes from generated code, dedupe the source list (map[string]bool) before iterating.
  3. If you intend multiple handlers, combine them into one handler chain or use router.Use(...) middleware instead of re-registering the path.
  4. Add a startup self-test that builds the router and asserts no panic; CI catches the regression before deploy.

Example fix

// before
router.GET("/healthz", healthHandler)
// ...elsewhere in the codebase...
router.GET("/healthz", healthHandler) // panics

// after — keep a single registration, share via middleware if needed
router.GET("/healthz", healthHandler)
Defensive patterns

Strategy: validation

Validate before calling

// Detect duplicate (method, path) registrations before the engine is built.
func assertNoDuplicates(routes []struct{ Method, Path string }) error {
    seen := make(map[string]string) // key "METHOD PATH" -> handler name
    for _, r := range routes {
        k := r.Method + " " + r.Path
        if _, ok := seen[k]; ok {
            return fmt.Errorf("duplicate route %s", k)
        }
        seen[k] = ""
    }
    return nil
}

Try / catch

func safeHandle(e *gin.Engine, method, path string, h gin.HandlerFunc) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("duplicate route %s %s: %v", method, path, r)
        }
    }()
    e.Handle(method, path, h)
    return nil
}

Prevention

When it happens

Trigger: Calling router.GET("/healthz", h) twice on the same Engine; calling router.Handle("GET", "/items", h) after a previous router.GET("/items", ...) (same method+path); mounting a sub-router via router.Group("").GET("/", h) when the parent already mapped "/"; or auto-generated route registration loops that emit the same entry more than once.

Common situations: Two init() blocks or two packages both registering the same health-check endpoint; route lists generated from config where a duplicate slips in; refactoring that moves a handler into a group without removing the old top-level call; merging PRs that each add the same route.

Related errors


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

Appendix: source

Thrown at tree.go:243

				pathSeg := path
				if n.nType != catchAll {
					pathSeg, _, _ = strings.Cut(pathSeg, "/")
				}
				prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
				panic("'" + pathSeg +
					"' in new path '" + fullPath +
					"' conflicts with existing wildcard '" + n.path +
					"' in existing prefix '" + prefix +
					"'")
			}

			n.insertChild(path, fullPath, handlers)
			return
		}

		// Otherwise add handle to current node
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}
		n.handlers = handlers
		n.fullPath = fullPath
		return
	}
}

// Search for a wildcard segment and check the name for invalid characters.
// Returns -1 as index, if no wildcard was found.
func findWildcard(path string) (wildcard string, i int, valid bool) {
	// Find start
	escapeColon := false
	for start, c := range []byte(path) {
		if escapeColon {
			escapeColon = false
			if c == ':' {
				continue
			}

View on GitHub (pinned to 34dac209ff)