{"record":{"id":"4e1e4a13698335b9","repo":"JuliusBrussee/caveman","slug":"body-must-be-a-json-object","errorCode":null,"errorMessage":"body must be a JSON object","messagePattern":"body must be a JSON object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"engine/pixel/transform_openai.go","lineNumber":690,"sourceCode":"\t\tCompress:         opts.Compress,\n\t\tCompressTools:    opts.CompressTools,\n\t\tMinCompressChars: opts.MinCompressChars,\n\t\tCols:             min(opts.Cols, ResolveGptProfile(model).StripCols),\n\t\tMultiCol:         1,\n\t\tCharsPerToken:    opts.CharsPerToken,\n\t\tReflow:           opts.Reflow,\n\t\tCollapseHistory:  opts.CollapseHistory,\n\t\tHistory:          history,\n\t}\n}\n\nfunc parseOpenAIRoot(body []byte) (map[string]json.RawMessage, error) {\n\tvar root map[string]json.RawMessage\n\tif err := json.Unmarshal(body, &root); err != nil {\n\t\treturn nil, err\n\t}\n\tif root == nil {\n\t\treturn nil, errors.New(\"body must be a JSON object\")\n\t}\n\treturn root, nil\n}\n\nfunc openAIModel(root map[string]json.RawMessage, opts TransformOptions) string {\n\tif raw, ok := root[\"model\"]; ok {\n\t\tvar model string\n\t\tif json.Unmarshal(raw, &model) == nil && model != \"\" {\n\t\t\treturn model\n\t\t}\n\t}\n\treturn opts.Model\n}\n\nfunc openAIChatContentText(content any) string {\n\tif s, ok := content.(string); ok {\n\t\treturn s\n\t}","sourceCodeStart":672,"sourceCodeEnd":708,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/engine/pixel/transform_openai.go#L672-L708","documentation":"parseOpenAIRoot unmarshals the request body into map[string]json.RawMessage. A syntactically valid JSON document that is not an object — `null`, an array, a string, a number — either fails Unmarshal or yields a nil map, which this guard turns into 'body must be a JSON object'. The OpenAI transform only operates on object-shaped payloads.","triggerScenarios":"Feeding the transform a JSON array (e.g. a batch), a bare `null`, or a scalar where a chat-completions-style object is required; a proxy bug that forwards the wrong body; truncated bodies that happen to parse as non-objects.","commonSituations":"Batch APIs misrouted through the single-request transform; clients sending `null` for an absent payload; middleware replacing the body with a JSON-encoded string.","solutions":["Ensure the caller sends a JSON object body ({...}) for this transform","Route array/batch payloads to the batch-capable path instead of the single-object transform","Log the first bytes of the offending body at the proxy boundary to find which upstream is malformed"],"exampleFix":"// before\nroot, err := parseOpenAIRoot(body) // body = `[{\"role\":\"user\",...}]`\n\n// after\ntrimmed := bytes.TrimSpace(body)\nif len(trimmed) == 0 || trimmed[0] != '{' {\n    return errors.New(\"expected a JSON object body\")\n}\nroot, err := parseOpenAIRoot(body)","handlingStrategy":"validation","validationCode":"if len(body) == 0 || body[0] != '{' {\n    return errors.New(\"request body must be a JSON object\")\n}\nroot, err := parseOpenAIRoot(body)","typeGuard":"func isJSONObject(b []byte) bool {\n    t := bytes.TrimSpace(b)\n    return len(t) > 0 && t[0] == '{'\n}","tryCatchPattern":"root, err := parseOpenAIRoot(body)\nif err != nil {\n    if !isJSONObject(body) {\n        // reject/reroute the payload (e.g. batch endpoint)\n    }\n    return err\n}","preventionTips":["Validate object shape at the proxy ingress before transforming","Keep single-request and batch routes strictly separated"],"tags":["pixel","go","openai","json","validation"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}