JuliusBrussee/caveman · error

multiple JSON values

Error message

multiple JSON values

What it means

decodeBedrockObject wraps a json.Decoder and rejects payloads that contain more than one JSON value. It decodes the target and then decodes again expecting io.EOF; a successful second decode means trailing data followed the top-level value. The Bedrock proxy layer throws this to guarantee strict single-document payloads before parsing usage data.

Solutions

  1. Split NDJSON payloads by newline and decode each line separately before calling decodeBedrockObject
  2. Trim or reject trailing non-whitespace bytes from the response body before decoding
  3. Log the body tail (last ~100 bytes) to identify the extra content source
  4. Verify streaming chunks are not concatenated into one buffer without a decoder per event

Example fix

// before: one decoder over concatenated NDJSON
var obj map[string]any
err := decodeBedrockObject(dec, &obj)
// after: decode each JSON line separately
for _, line := range bytes.Split(body, []byte("\n")) {
    if len(bytes.TrimSpace(line)) == 0 { continue }
    dec := json.NewDecoder(bytes.NewReader(line))
    if err := decodeBedrockObject(dec, &obj); err != nil { return err }
}
Defensive patterns

Strategy: validation

Validate before calling

if !json.Valid(bytes.TrimSpace(body)) { return fmt.Errorf("payload is not a single JSON document") }
if bytes.Contains(bytes.TrimSpace(body)[1:len(body)-1], []byte("}{")) { return fmt.Errorf("multiple JSON documents concatenated") }

Try / catch

var obj map[string]any
if err := decodeBedrockObject(dec, &obj); err != nil {
    if strings.Contains(err.Error(), "multiple JSON values") {
        // split the stream and decode per-document
    }
    return err
}

Prevention

When it happens

Trigger: Bedrock response/converse payload contains trailing bytes after the top-level JSON object, e.g. two concatenated JSON objects, trailing garbage, or NDJSON-style lines fed into a single-decode path.

Common situations: Streaming/NDJSON responses (multiple JSON lines) being passed as one document; upstream providers appending extra data; hand-concatenated JSON in tests or fixtures; chunked body assembly accidentally duplicating a payload.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/9742e4f2d60e834d. Report an issue: GitHub.

Appendix: source

Thrown at proxy/providers/bedrock/usage.go:601

		usage.Malformed = true
		return
	}
	dst[snake] = value
}

// decodeBedrockObject preserves integer lexemes as json.Number. Decoding into
// float64 first loses precision above 2^53 and could turn an invalid/overflowed
// provider counter into a plausible billable integer.
func decodeBedrockObject(data []byte, dst *map[string]any) error {
	dec := json.NewDecoder(bytes.NewReader(data))
	dec.UseNumber()
	if err := dec.Decode(dst); err != nil {
		return err
	}
	var trailing any
	if err := dec.Decode(&trailing); err != io.EOF {
		if err == nil {
			return fmt.Errorf("multiple JSON values")
		}
		return err
	}
	return nil
}

// bedrockUsageObjects returns every usage-bearing sub-object reachable from obj.
// Bedrock places usage at the top level ("usage") for both Converse and
// InvokeModel; streamed converse emits it under "metadata".
func bedrockUsageObjects(obj map[string]any) []map[string]any {
	var out []map[string]any
	add := func(v any) {
		if m, ok := v.(map[string]any); ok {
			out = append(out, m)
		}
	}
	add(obj["usage"])
	if meta, ok := obj["metadata"].(map[string]any); ok {

View on GitHub (pinned to 3ee70a1026)