{"record":{"id":"dfc2655a0ffc98f9","repo":"caddyserver/caddy","slug":"duplicate-id-s-found-at-s-and-s","errorCode":null,"errorMessage":"duplicate ID '%s' found at %s and %s","messagePattern":"duplicate ID '(.+?)' found at (.+?) and (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"caddy.go","lineNumber":305,"sourceCode":"// \"@id\" and maps that ID value to the full configPath in the index.\n// This function is NOT safe for concurrent access; obtain a write lock\n// on currentCtxMu.\nfunc indexConfigObjects(ptr any, configPath string, index map[string]string) error {\n\tswitch val := ptr.(type) {\n\tcase map[string]any:\n\t\tfor k, v := range val {\n\t\t\tif k == idKey {\n\t\t\t\tvar idStr string\n\t\t\t\tswitch idVal := v.(type) {\n\t\t\t\tcase string:\n\t\t\t\t\tidStr = idVal\n\t\t\t\tcase float64: // all JSON numbers decode as float64\n\t\t\t\t\tidStr = fmt.Sprintf(\"%v\", idVal)\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\"%s: %s field must be a string or number\", configPath, idKey)\n\t\t\t\t}\n\t\t\t\tif existingPath, ok := index[idStr]; ok {\n\t\t\t\t\treturn fmt.Errorf(\"duplicate ID '%s' found at %s and %s\", idStr, existingPath, configPath)\n\t\t\t\t}\n\t\t\t\tindex[idStr] = configPath\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// traverse this object property recursively\n\t\t\terr := indexConfigObjects(val[k], path.Join(configPath, k), index)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\tcase []any:\n\t\t// traverse each element of the array recursively\n\t\tfor i := range val {\n\t\t\terr := indexConfigObjects(val[i], path.Join(configPath, strconv.Itoa(i)), index)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/caddy.go#L287-L323","documentation":"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.","triggerScenarios":"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.","commonSituations":"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\".","solutions":["Rename one of the duplicates — the error gives both paths.","In generators, derive @id from the loop variable (e.g. \"@id\": \"route-\" + name).","Watch for number/string normalization: 1 and \"1\" are the same ID.","Remove @id fields that are not used for /id/ addressing."],"exampleFix":"// before\n{\"@id\":\"upstream\", \"handle\":[{\"@id\":\"upstream\"}]}\n\n// after\n{\"@id\":\"upstream\", \"handle\":[{\"@id\":\"upstream-0\"}]}","handlingStrategy":"validation","validationCode":"ids := map[string]string{}\nvar walk func(n any, p string) error\nwalk = func(n any, p string) error {\n    if m, ok := n.(map[string]any); ok {\n        if raw, ok := m[\"@id\"]; ok {\n            key := fmt.Sprint(raw) // numbers normalize like Caddy does\n            if prev, dup := ids[key]; dup {\n                return fmt.Errorf(\"duplicate @id %q at %s and %s\", key, prev, p)\n            }\n            ids[key] = p\n        }\n        for k, v := range m { if err := walk(v, p+\"/\"+k); err != nil { return err } }\n    }\n    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 } } }\n    return nil\n}\nerr := walk(decodedCfg, \"\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Derive @ids from unique keys when generating configs.","Remember 1 and \"1\" collide after normalization.","Only add @id where you actually use /id/ addressing."],"tags":["caddy","admin-api","config","id-index","duplicate","validation"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}