caddyserver/caddy · error · APIError

indexing config: %v

Error message

indexing config: %v

What it means

changeConfig walks the new config tree with indexConfigObjects to register every '@id' field so /id/... admin paths can address config objects. Any object whose '@id' is not a string or number, or any duplicate '@id', aborts the change with this APIError (HTTP 400). On this path Caddy first restores rawCfg from the last good encoded copy, so the running config is unaffected.

Source

Thrown at caddy.go:241

	}

	// find any IDs in this config and index them
	idx := make(map[string]string)
	err = indexConfigObjects(rawCfg[rawConfigKey], "/"+rawConfigKey, idx)
	if err != nil {
		if len(rawCfgJSON) > 0 {
			var oldCfg any
			err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
			if err2 != nil {
				err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
			}
			rawCfg[rawConfigKey] = oldCfg
		} else {
			rawCfg[rawConfigKey] = nil
		}
		return APIError{
			HTTPStatus: http.StatusBadRequest,
			Err:        fmt.Errorf("indexing config: %v", err),
		}
	}

	// load this new config; if it fails, we need to revert to
	// our old representation of caddy's actual config
	err = unsyncedDecodeAndRun(newCfg, true)
	if err != nil {
		if len(rawCfgJSON) > 0 {
			// restore old config state to keep it consistent
			// with what caddy is still running; we need to
			// unmarshal it again because it's likely that
			// pointers deep in our rawCfg map were modified
			var oldCfg any
			err2 := json.Unmarshal(rawCfgJSON, &oldCfg)
			if err2 != nil {
				err = fmt.Errorf("%v; additionally, restoring old config: %v", err, err2)
			}
			rawCfg[rawConfigKey] = oldCfg

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Make every '@id' a unique string (or number) — e.g. "@id": "srv0-tls".
  2. Search the submitted JSON for duplicate '"@id"' keys sharing the same value and rename them.
  3. Remove '@id' fields you do not need for /id/ addressing.

Example fix

// before
{ "@id": true, "apps": { ... } }

// after
{ "@id": "main", "apps": { ... } }
Defensive patterns

Strategy: validation

Validate before calling

// Before submitting, check every @id is string/number and unique.
func checkIDs(node any, seen map[string]string, path string) error {
    m, ok := node.(map[string]any)
    if !ok { return nil }
    if id, ok := m["@id"]; ok {
        s, ok := id.(string)
        if !ok { _, ok = id.(float64) }
        if !ok { return fmt.Errorf("%s: @id not string/number", path) }
        if _, dup := seen[fmt.Sprint(id)]; dup { return fmt.Errorf("duplicate @id %v at %s", id, path) }
        seen[fmt.Sprint(id)] = path
    }
    for k, v := range m { if err := checkIDs(v, seen, path+"/"+k); err != nil { return err } }
    return nil
}

Prevention

When it happens

Trigger: POSTing/PUTing a JSON config where an '@id' value is an object, array, boolean, or null; assigning the same '@id' string to two objects; PATCHing /config/ subtrees that introduce colliding IDs. Does not apply to Caddyfile-adapted configs unless they emit @id.

Common situations: Hand-written JSON configs with '@id': true or nested objects; generated configs where a template reuses an id; merging config fragments that each carry '@id': 'my-app'.

Related errors


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