{"record":{"id":"601376bf42dd20af","repo":"JuliusBrussee/caveman","slug":"json-number-exponent-exceeds-supported-range","errorCode":null,"errorMessage":"JSON number exponent exceeds supported range","messagePattern":"JSON number exponent exceeds supported range","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/proposalrun/proposalrun.go","lineNumber":289,"sourceCode":"\tif len(raw) == 0 {\n\t\treturn 0, fmt.Errorf(\"invalid JSON number exponent\")\n\t}\n\tnegative := false\n\tif raw[0] == '+' || raw[0] == '-' {\n\t\tnegative = raw[0] == '-'\n\t\traw = raw[1:]\n\t}\n\tif len(raw) == 0 {\n\t\treturn 0, fmt.Errorf(\"invalid JSON number exponent\")\n\t}\n\tvalue := 0\n\tfor i := 0; i < len(raw); i++ {\n\t\tif raw[i] < '0' || raw[i] > '9' {\n\t\t\treturn 0, fmt.Errorf(\"invalid JSON number exponent\")\n\t\t}\n\t\tdigit := int(raw[i] - '0')\n\t\tif value > (maxJSONBNumberExponent-digit)/10 {\n\t\t\treturn 0, fmt.Errorf(\"JSON number exponent exceeds supported range\")\n\t\t}\n\t\tvalue = value*10 + digit\n\t}\n\tif negative {\n\t\treturn -value, nil\n\t}\n\treturn value, nil\n}\n\n// formatTime pins the created_at representation: UTC, microsecond resolution\n// (Postgres TIMESTAMPTZ stores microseconds, so truncating here makes the value\n// hashed at write time equal the value read back at verify time), RFC3339Nano.\nfunc formatTime(t time.Time) string {\n\treturn t.UTC().Truncate(time.Microsecond).Format(time.RFC3339Nano)\n}\n\n// RowHash computes the pinned canonical row hash:\n//","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/proposalrun/proposalrun.go#L271-L307","documentation":"The exponent overflow guard in proposalrun's JSON number canonicalizer. maxJSONBNumberExponent is 131072+16383 = 147455, mirroring PostgreSQL's jsonb numeric limits (131072 integer digits, 16383 fraction digits). While accumulating the exponent digits left-to-right, the parser checks value > (max-digit)/10 before each multiply-by-10 step; if the running value would exceed the cap it refuses rather than silently wrapping or letting a hostile exponent drive unbounded arithmetic later.","triggerScenarios":"Any canonicalization of a JSON number whose exponent magnitude exceeds 147455, e.g. 1e999999 or 1e-1000000, while computing RowHash or verifying the chain.","commonSituations":"Round-tripping values produced by arbitrary-precision libraries (big.Float/Decimal with huge exponents) into a proposal Detail; adversarial input crafted to blow up jsonb storage in Postgres; porting data from systems that allow larger exponents than Postgres jsonb.","solutions":["Clamp or reject numbers with |exponent| > 147455 before they enter Detail — this ceiling is a hard Postgres jsonb limit, not a tunable.","If huge magnitudes are legitimate, store the value as a string in Detail rather than a JSON number.","Validate with a decimal library (e.g. shopspring/decimal or math/big) and quantize/serialize before marshaling the run row."],"exampleFix":"// before\ndetail := []byte(`{\"prob\": 1e-1000000}`)\n_, err := proposalrun.RowHash(prev, seq, action, detail, cost, createdAt)\n\n// after\n// serialize out-of-range magnitudes as strings\ndetail := []byte(`{\"prob\": \"1e-1000000\"}`)","handlingStrategy":"validation","validationCode":"const maxExp = 131072 + 16383 // mirror of maxJSONBNumberExponent\nfunc exponentInRange(f float64) bool {\n    if f == 0 { return true }\n    _, exp := strconv.FormatFloat(f, 'e', -1, 64) // parse exponent via formatting\n    _ = exp\n    s := strconv.FormatFloat(f, 'e', -1, 64)\n    i := strings.IndexAny(s, \"eE\")\n    n, _ := strconv.Atoi(s[i+1:])\n    return n >= -maxExp && n <= maxExp\n}","typeGuard":null,"tryCatchPattern":"if err := proposalrun.VerifyChain(runs); err != nil {\n    if strings.Contains(err.Error(), \"exponent exceeds supported range\") {\n        // re-serialize the offending Detail with out-of-range numbers as strings\n    }\n}","preventionTips":["Validate numeric ranges before putting values into Detail; serialize out-of-range magnitudes as strings.","Treat 147455 as a hard Postgres jsonb ceiling when designing the Detail schema.","If big-precision data is expected, use math/big or shopspring/decimal and quantize before marshal."],"tags":["json","postgres","limits","hash-chain"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}