caddyserver/caddy · error

duplicate ID '%s' found at %s and %s

Error message

duplicate ID '%s' found at %s and %s

What it means

indexConfigObjects builds a global map of '@id' value -> config path so the admin API can route /id/<id>/... requests. If the same '@id' string (after number-to-string normalization) appears at two different paths, the second occurrence fails with "duplicate ID '<id>' found at <path1> and <path2>", rejecting the config change. IDs must be unique across the entire config, not just within a scope.

Source

Thrown at caddy.go:305

// "@id" and maps that ID value to the full configPath in the index.
// This function is NOT safe for concurrent access; obtain a write lock
// on currentCtxMu.
func indexConfigObjects(ptr any, configPath string, index map[string]string) error {
	switch val := ptr.(type) {
	case map[string]any:
		for k, v := range val {
			if k == idKey {
				var idStr string
				switch idVal := v.(type) {
				case string:
					idStr = idVal
				case float64: // all JSON numbers decode as float64
					idStr = fmt.Sprintf("%v", idVal)
				default:
					return fmt.Errorf("%s: %s field must be a string or number", configPath, idKey)
				}
				if existingPath, ok := index[idStr]; ok {
					return fmt.Errorf("duplicate ID '%s' found at %s and %s", idStr, existingPath, configPath)
				}
				index[idStr] = configPath
				continue
			}
			// traverse this object property recursively
			err := indexConfigObjects(val[k], path.Join(configPath, k), index)
			if err != nil {
				return err
			}
		}
	case []any:
		// traverse each element of the array recursively
		for i := range val {
			err := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)
			if err != nil {
				return err
			}
		}

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Rename one of the duplicates — the error gives both paths.
  2. In generators, derive @id from the loop variable (e.g. "@id": "route-" + name).
  3. Watch for number/string normalization: 1 and "1" are the same ID.
  4. Remove @id fields that are not used for /id/ addressing.

Example fix

// before
{"@id":"upstream", "handle":[{"@id":"upstream"}]}

// after
{"@id":"upstream", "handle":[{"@id":"upstream-0"}]}
Defensive patterns

Strategy: validation

Validate before calling

ids := map[string]string{}
var walk func(n any, p string) error
walk = func(n any, p string) error {
    if m, ok := n.(map[string]any); ok {
        if raw, ok := m["@id"]; ok {
            key := fmt.Sprint(raw) // numbers normalize like Caddy does
            if prev, dup := ids[key]; dup {
                return fmt.Errorf("duplicate @id %q at %s and %s", key, prev, p)
            }
            ids[key] = p
        }
        for k, v := range m { if err := walk(v, p+"/"+k); err != nil { return err } }
    }
    if a, ok := n.([]any); ok { for i, e := range a { if err := walk(e, fmt.Sprintf("%s/%d", p, i)); err != nil { return err } } }
    return nil
}
err := walk(decodedCfg, "")

Prevention

When it happens

Trigger: Two objects anywhere in the JSON config sharing "@id": "my-id"; array items templated with a constant @id; merging two config fragments that each define "@id": "apps"; numeric 1 and string "1" collide because numbers are normalized via %v.

Common situations: Config generators looping over sites without varying the ID; manual copy-paste of route blocks; config merging tools; subtle collisions like "@id": 0 vs "@id": "0".

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/dfc2655a0ffc98f9. Report an issue: GitHub.