JuliusBrussee/caveman · error
proposalrun: marshal canonical detail
Error message
proposalrun: marshal canonical detail: %w
What it means
CanonicalDetail wraps a failure from json.Marshal of the already-canonicalized value. In normal operation this is nearly unreachable: encoding/json can fail marshaling only for unsupported values (channels, funcs, cyclic maps) or a MarshalJSON that errors. Because CanonicalDetail only rebuilds maps, slices, strings, bools, and canonicalJSONNumber (whose MarshalJSON writes its own validated bytes), hitting this means an internal invariant was violated rather than bad user input.
Solutions
- Check the wrapped inner error from json.Marshal for the unsupported-type message; identify which Go type leaked into the canonicalized tree.
- Verify you are on an unmodified version of the proposalrun package; if you patched canonicalizeJSONValue, ensure every new branch returns only JSON-safe types (map, slice, string, bool, nil, canonicalJSONNumber).
- If reproducible on stock code, file a bug with the exact detail blob — this indicates an internal invariant violation.
- As a defensive measure, log the offending detail blob (truncated) when this wrapper fires so the failing value can be reproduced.
Example fix
// before (patched canonicalize adding a raw type)
case time.Time:
return x, nil // json.Marshal now fails on... actually encodes, but any custom Marshaler error propagates here
// after
case time.Time:
return x.UTC().Format(time.RFC3339Nano), nil // return plain JSON-safe string Defensive patterns
Strategy: try-catch
Try / catch
canon, err := proposalrun.CanonicalDetail(detail)
if err != nil && strings.Contains(err.Error(), "marshal canonical detail") {
// invariant violation, not bad input: alert and dump the blob for a bug report
log.Printf("BUG: canonical marshal failed: %v; blob=%q", err, truncate(detail, 512))
return err
} Prevention
- Do not modify canonicalizeJSONValue to return non-JSON-safe Go types.
- Keep canonicalJSONNumber's MarshalJSON free of fallible work.
- Treat any occurrence of this error as an internal bug and file it with the offending blob.
- Pin the proposalrun package version; re-run hash-chain tests after any upgrade.
When it happens
Trigger: Calling CanonicalDetail/RowHash where the decoded-and-rebuilt value tree somehow contains a non-JSON-representable Go value or canonicalJSONNumber produces bytes json.Marshal rejects. In practice this surfaces only if the package's internal invariants break (e.g. a future code change injects an unsupported type into the rebuilt tree).
Common situations: After a code change to canonicalizeJSONValue that introduces a new Go type without a MarshalJSON; custom json.Marshaler implementations registered on wrapper types passed through the any tree; OOM/corrupted state causing a malformed canonicalJSONNumber string.
Understand the failure class
Background: json.Marshal / "failed to marshal" errors in Go: why "unsupported type" happens and how to fix it — this error's family across 22 libraries.
Related errors
- proposalrun: canonicalize detail
- envelope: encode scope
- envelope: marshal metadata
- envelope: parse metadata
- invalid JSON number
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/be03b96e28bdc589.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/proposalrun/proposalrun.go:79
dec := json.NewDecoder(bytes.NewReader(detail))
dec.UseNumber()
if err := dec.Decode(&v); err != nil {
return nil, fmt.Errorf("proposalrun: detail is not valid JSON: %w", err)
}
var trailing any
if err := dec.Decode(&trailing); err != io.EOF {
if err == nil {
return nil, fmt.Errorf("proposalrun: detail contains trailing JSON")
}
return nil, fmt.Errorf("proposalrun: detail contains trailing data: %w", err)
}
v, err := canonicalizeJSONValue(v)
if err != nil {
return nil, fmt.Errorf("proposalrun: canonicalize detail: %w", err)
}
out, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("proposalrun: marshal canonical detail: %w", err)
}
return out, nil
}
// PostgreSQL jsonb stores JSON numbers using its numeric type. These are the
// documented numeric bounds (131072 digits before and 16383 after the decimal
// point); rejecting at the canonicalization boundary keeps write and read-back
// hashing deterministic and avoids unbounded strings from hostile exponents.
// maxJSONBNumberExponent is an additional allocation guard. PostgreSQL can
// accept some zero literals with a larger positive exponent after normalizing
// their zero weight; this package rejects those spellings conservatively so a
// hostile exponent never drives unbounded intermediate arithmetic.
const (
maxJSONBNumericIntegerDigits = 131072
maxJSONBNumericFractionDigits = 16383
maxJSONBNumberCoefficient = maxJSONBNumericIntegerDigits + maxJSONBNumericFractionDigits + 1
maxJSONBNumberExponent = maxJSONBNumericIntegerDigits + maxJSONBNumericFractionDigits
maxJSONBNumberLexeme = maxJSONBNumberCoefficient + 16View on GitHub (pinned to 3ee70a1026)