{"record":{"id":"be03b96e28bdc589","repo":"JuliusBrussee/caveman","slug":"proposalrun-marshal-canonical-detail-w","errorCode":null,"errorMessage":"proposalrun: marshal canonical detail: %w","messagePattern":"proposalrun: marshal canonical detail: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":79,"sourceCode":"\tdec := json.NewDecoder(bytes.NewReader(detail))\n\tdec.UseNumber()\n\tif err := dec.Decode(&v); err != nil {\n\t\treturn nil, fmt.Errorf(\"proposalrun: detail is not valid JSON: %w\", err)\n\t}\n\tvar trailing any\n\tif err := dec.Decode(&trailing); err != io.EOF {\n\t\tif err == nil {\n\t\t\treturn nil, fmt.Errorf(\"proposalrun: detail contains trailing JSON\")\n\t\t}\n\t\treturn nil, fmt.Errorf(\"proposalrun: detail contains trailing data: %w\", err)\n\t}\n\tv, err := canonicalizeJSONValue(v)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"proposalrun: canonicalize detail: %w\", err)\n\t}\n\tout, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"proposalrun: marshal canonical detail: %w\", err)\n\t}\n\treturn out, nil\n}\n\n// PostgreSQL jsonb stores JSON numbers using its numeric type. These are the\n// documented numeric bounds (131072 digits before and 16383 after the decimal\n// point); rejecting at the canonicalization boundary keeps write and read-back\n// hashing deterministic and avoids unbounded strings from hostile exponents.\n// maxJSONBNumberExponent is an additional allocation guard. PostgreSQL can\n// accept some zero literals with a larger positive exponent after normalizing\n// their zero weight; this package rejects those spellings conservatively so a\n// hostile exponent never drives unbounded intermediate arithmetic.\nconst (\n\tmaxJSONBNumericIntegerDigits  = 131072\n\tmaxJSONBNumericFractionDigits = 16383\n\tmaxJSONBNumberCoefficient     = maxJSONBNumericIntegerDigits + maxJSONBNumericFractionDigits + 1\n\tmaxJSONBNumberExponent        = maxJSONBNumericIntegerDigits + maxJSONBNumericFractionDigits\n\tmaxJSONBNumberLexeme          = maxJSONBNumberCoefficient + 16","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/proposalrun/proposalrun.go#L61-L97","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before (patched canonicalize adding a raw type)\ncase time.Time:\n    return x, nil // json.Marshal now fails on... actually encodes, but any custom Marshaler error propagates here\n\n// after\ncase time.Time:\n    return x.UTC().Format(time.RFC3339Nano), nil // return plain JSON-safe string","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"canon, err := proposalrun.CanonicalDetail(detail)\nif err != nil && strings.Contains(err.Error(), \"marshal canonical detail\") {\n    // invariant violation, not bad input: alert and dump the blob for a bug report\n    log.Printf(\"BUG: canonical marshal failed: %v; blob=%q\", err, truncate(detail, 512))\n    return err\n}","preventionTips":["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."],"tags":["go","json","marshaling","hash-chain","invariant"],"backgroundTag":"json-marshal-failed","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}