{"record":{"id":"7d963d969d9e9269","repo":"JuliusBrussee/caveman","slug":"cacheengine-request-root-must-be-object","errorCode":null,"errorMessage":"cacheengine: request root must be object","messagePattern":"cacheengine: request root must be object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cacheengine/native.go","lineNumber":480,"sourceCode":"\tif err != nil {\n\t\treturn false, false\n\t}\n\t_, err = decoder.Token()\n\treturn errors.Is(err, io.EOF), found\n}\n\nfunc inspectUniqueJSONValue(decoder *json.Decoder, root bool, depth int, provider string, path []string) (bool, error) {\n\tif depth > 512 {\n\t\treturn false, errors.New(\"cacheengine: JSON nesting limit exceeded\")\n\t}\n\ttoken, err := decoder.Token()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\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)","sourceCodeStart":462,"sourceCodeEnd":498,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/cacheengine/native.go#L462-L498","documentation":"Thrown by inspectUniqueJSONValue when the ROOT JSON token is a scalar (string, number, bool, null) rather than an object or array delimiter. The engine only accepts JSON requests whose top level is an object, because provider request bodies are always objects and cache-marker lookup keys off object paths.","triggerScenarios":"Sending a body like \"42\", '\"text\"', 'true', or 'null' to the native engine; typically a client bug that marshals an inner value instead of the request struct, or writes a bare JSON fragment.","commonSituations":"json.Marshal on the wrong variable (a field instead of the struct); string interpolation where a pre-encoded fragment is sent directly; proxy or test harness posting a raw scalar fixture; note the wrapper may swallow this error and just report no cache marker found.","solutions":["Marshal the full request struct, not one of its fields","If hand-building the body, ensure it starts with '{' — validate with a quick root-type check before sending","Check test fixtures: a truncated .json file often starts with a scalar"],"exampleFix":"// before\nbody, _ := json.Marshal(req.Messages) // array, not the request object\n\n// after\nbody, _ := json.Marshal(req) // {\"model\":..., \"messages\":[...]}","handlingStrategy":"validation","validationCode":"func rootIsJSONObject(b []byte) bool {\n    t := bytes.TrimLeft(b, \" \\t\\r\\n\")\n    return len(t) > 0 && t[0] == '{'\n}","typeGuard":"// n/a","tryCatchPattern":null,"preventionTips":["Always marshal the full request struct, never a field","Add a root-type smoke check in tests for hand-built bodies"],"tags":["cacheengine","json","validation","protocol","go"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}