{"record":{"id":"ea4165c08c180dc5","repo":"JuliusBrussee/caveman","slug":"cacheengine-duplicate-or-invalid-object-key","errorCode":null,"errorMessage":"cacheengine: duplicate or invalid object key","messagePattern":"cacheengine: duplicate or invalid object key","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cacheengine/native.go","lineNumber":495,"sourceCode":"\tdelim, composite := token.(json.Delim)\n\tif !composite {\n\t\tif root {\n\t\t\treturn false, errors.New(\"cacheengine: request root must be object\")\n\t\t}\n\t\treturn false, nil\n\t}\n\tswitch delim {\n\tcase '{':\n\t\tseen := map[string]bool{}\n\t\tfound := false\n\t\tfor decoder.More() {\n\t\t\tkeyToken, err := decoder.Token()\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tkey, ok := keyToken.(string)\n\t\t\tif !ok || seen[key] {\n\t\t\t\treturn false, errors.New(\"cacheengine: duplicate or invalid object key\")\n\t\t\t}\n\t\t\tseen[key] = true\n\t\t\tmatched := cacheMarkerAt(provider, path, key)\n\t\t\tpath = append(path, key)\n\t\t\tchildFound, err := inspectUniqueJSONValue(decoder, false, depth+1, provider, path)\n\t\t\tpath = path[:len(path)-1]\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t\tfound = found || matched || childFound\n\t\t}\n\t\tclosing, err := decoder.Token()\n\t\tif err != nil || closing != json.Delim('}') {\n\t\t\treturn false, errors.New(\"cacheengine: invalid object close\")\n\t\t}\n\t\treturn found, nil\n\tcase '[':\n\t\tif root {","sourceCodeStart":477,"sourceCodeEnd":513,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/cacheengine/native.go#L477-L513","documentation":"Thrown by inspectUniqueJSONValue while iterating an object's members when a key token is not a JSON string (impossible in valid JSON, so effectively a decoder-state bug) or when the same key appears twice in one object. The engine enforces key uniqueness because duplicate keys make cache-marker path matching ambiguous.","triggerScenarios":"A body containing an object with a repeated key, e.g. {\"model\":\"a\", ..., \"model\":\"b\"} — some JSON writers (hand-built string concatenation, lenient serializers, naive map merging) emit duplicates; Go's encoding/json cannot produce them from a map, but manual construction or upstream proxies can.","commonSituations":"Merging two request JSONs by string concatenation instead of decoding/merging maps; hand-rolled JSON templating in scripts or tests; a middleware that appends a field (e.g. a cache marker) to an already-serialized body that contains it.","solutions":["Decode conflicting JSONs into a map, merge, then re-marshal so keys are unique","Stop appending serialized fragments to an existing body string; build the object once","If injecting a cache marker, check the body does not already contain that key"],"exampleFix":"// before\nbody := rawBody + \",\\\"cache_marker\\\":\\\"x\\\"}\" // rawBody may already contain cache_marker\n\n// after\nvar m map[string]any\njson.Unmarshal(rawBodyBytes, &m)\nm[\"cache_marker\"] = \"x\"\nbody, _ := json.Marshal(m)","handlingStrategy":"validation","validationCode":"var probe map[string]json.RawMessage\nif err := json.Unmarshal(body, &probe); err != nil { return err } // duplicate keys collapse silently but at least proves validity; for strictness use a token scanner that tracks seen keys per object","typeGuard":"// n/a","tryCatchPattern":null,"preventionTips":["Never merge JSON by string concatenation; decode, merge maps, re-marshal","Check for an existing key before injecting markers"],"tags":["cacheengine","json","duplicate-keys","protocol","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}