caddyserver/caddy · error · APIError
indexing config
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.
Solutions
- Make every '@id' a unique string (or number) — e.g. "@id": "srv0-tls".
- Search the submitted JSON for duplicate '"@id"' keys sharing the same value and rename them.
- 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
- Use only unique string @id values.
- Lint generated configs for duplicate or non-scalar @id before POSTing.
- Prefer caddy validate on the JSON config before loading it via the API.
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
- duplicate ID ' ' found at and
- : field must be a string or number
- must be exactly one listener address; cannot listen on
- parsing listener address
- ; additionally, restoring old config
AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15).
Data as JSON: /api/errors/b7d597df72464d84.
Report an issue: GitHub.
Appendix: 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] = oldCfgView on GitHub (pinned to 50e54ee279)