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
- Split NDJSON payloads by newline and decode each line separately before calling decodeBedrockObject
- Trim or reject trailing non-whitespace bytes from the response body before decoding
- Log the body tail (last ~100 bytes) to identify the extra content source
- 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
- Treat streaming/NDJSON responses as event streams, never as one JSON document
- Trim trailing whitespace-only bytes, reject other trailing data before decode
- Add a fixture test with concatenated JSON to pin the strict behavior
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
- envelope: encode scope
- envelope: marshal metadata
- envelope: parse metadata
- githubapp: decode repo installation
- githubapp: decode repo
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)