JuliusBrussee/caveman · error

body must be a JSON object

Error message

body must be a JSON object

What it means

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.

Source

Thrown at engine/pixel/transform_openai.go:690

		Compress:         opts.Compress,
		CompressTools:    opts.CompressTools,
		MinCompressChars: opts.MinCompressChars,
		Cols:             min(opts.Cols, ResolveGptProfile(model).StripCols),
		MultiCol:         1,
		CharsPerToken:    opts.CharsPerToken,
		Reflow:           opts.Reflow,
		CollapseHistory:  opts.CollapseHistory,
		History:          history,
	}
}

func parseOpenAIRoot(body []byte) (map[string]json.RawMessage, error) {
	var root map[string]json.RawMessage
	if err := json.Unmarshal(body, &root); err != nil {
		return nil, err
	}
	if root == nil {
		return nil, errors.New("body must be a JSON object")
	}
	return root, nil
}

func openAIModel(root map[string]json.RawMessage, opts TransformOptions) string {
	if raw, ok := root["model"]; ok {
		var model string
		if json.Unmarshal(raw, &model) == nil && model != "" {
			return model
		}
	}
	return opts.Model
}

func openAIChatContentText(content any) string {
	if s, ok := content.(string); ok {
		return s
	}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Ensure the caller sends a JSON object body ({...}) for this transform
  2. Route array/batch payloads to the batch-capable path instead of the single-object transform
  3. Log the first bytes of the offending body at the proxy boundary to find which upstream is malformed

Example fix

// before
root, err := parseOpenAIRoot(body) // body = `[{"role":"user",...}]`

// after
trimmed := bytes.TrimSpace(body)
if len(trimmed) == 0 || trimmed[0] != '{' {
    return errors.New("expected a JSON object body")
}
root, err := parseOpenAIRoot(body)
Defensive patterns

Strategy: validation

Validate before calling

if len(body) == 0 || body[0] != '{' {
    return errors.New("request body must be a JSON object")
}
root, err := parseOpenAIRoot(body)

Type guard

func isJSONObject(b []byte) bool {
    t := bytes.TrimSpace(b)
    return len(t) > 0 && t[0] == '{'
}

Try / catch

root, err := parseOpenAIRoot(body)
if err != nil {
    if !isJSONObject(body) {
        // reject/reroute the payload (e.g. batch endpoint)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: Batch APIs misrouted through the single-request transform; clients sending `null` for an absent payload; middleware replacing the body with a JSON-encoded string.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/4e1e4a13698335b9. Report an issue: GitHub.