{"record":{"id":"65d25e26b3e57728","repo":"windmill-labs/windmill","slug":"invalid-json-float","errorCode":null,"errorMessage":"invalid json-float","messagePattern":"invalid json-float","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/windmill-worker/src/pg_executor.rs","lineNumber":1812,"sourceCode":"    // JSON has no encoding for NaN / +Inf / -Inf, but Postgres `float4` /\n    // `float8` (and `numeric`, via the special `'NaN'` value) do return them.\n    // Pre-fix the worker errored with \"invalid json-float\", failing the\n    // entire query. Round-trip these as JSON strings (\"NaN\", \"Infinity\",\n    // \"-Infinity\") so the rest of the row still comes through; users who\n    // need numeric semantics can filter them out client-side.\n    let f64_to_json_number = |raw_val: f64| -> Result<JSONValue, Error> {\n        if raw_val.is_nan() {\n            return Ok(JSONValue::String(\"NaN\".to_string()));\n        }\n        if raw_val.is_infinite() {\n            return Ok(JSONValue::String(if raw_val > 0.0 {\n                \"Infinity\".to_string()\n            } else {\n                \"-Infinity\".to_string()\n            }));\n        }\n        let temp =\n            serde_json::Number::from_f64(raw_val).ok_or(anyhow::anyhow!(\"invalid json-float\"))?;\n        Ok(JSONValue::Number(temp))\n    };\n    Ok(match *column.type_() {\n        // for rust-postgres <> postgres type-mappings: https://docs.rs/postgres/latest/postgres/types/trait.FromSql.html#types\n        // for postgres types: https://www.postgresql.org/docs/7.4/datatype.html#DATATYPE-TABLE\n\n        // single types\n        Type::BOOL => get_basic(row, column, column_i, |a: bool| Ok(JSONValue::Bool(a)))?,\n        Type::BIT => get_basic(row, column, column_i, |a: bit_vec::BitVec| match a.len() {\n            1 => Ok(JSONValue::Bool(a.get(0).unwrap())),\n            _ => Ok(JSONValue::String(\n                a.iter()\n                    .map(|x| if x { \"1\" } else { \"0\" })\n                    .collect::<String>(),\n            )),\n        })?,\n        Type::INT2 => get_basic(row, column, column_i, |a: i16| {\n            Ok(JSONValue::Number(serde_json::Number::from(a)))","sourceCodeStart":1794,"sourceCodeEnd":1830,"githubUrl":"https://github.com/windmill-labs/windmill/blob/e474e8803ce2ff5c2df09a58dab51d45f5c922ca/backend/windmill-worker/src/pg_executor.rs#L1794-L1830","documentation":"pg_cell_to_json_value_with_state converts a Postgres result cell (FLOAT4/FLOAT8 column) into a JSON value. Rust f64 values that are NaN or +/-Infinity have no JSON representation, so serde_json::Number::from_f64 returns None and the code raises 'invalid json-float' instead of silently emitting invalid JSON. It exists to keep job results valid JSON.","triggerScenarios":"A query returns a float column containing NaN, 'Infinity' or '-Infinity' (e.g. INSERT of 'Infinity'::float8 or a computation producing NaN like log(-1), 0/0 via PG float ops, or power(0,-1)) and the row is converted to the step's result JSON.","commonSituations":"Analytics columns storing PG 'Infinity' sentinels for missing timestamps/rates; divisions by zero on float columns (PG yields Infinity rather than erroring); aggregation results over empty partitions producing NaN.","solutions":["Coerce non-finite floats to NULL in the query: `CASE WHEN NOT (col::float8 = 'Infinity' OR col::float8 = '-Infinity' OR col::float8 <> col) THEN col END`, or use `CASE WHEN col::text ~ 'Inf|NaN' THEN NULL ELSE col END`","Prevent NaN/Inf at write time by validating inputs before INSERT","If sentinels are intentional, cast the column to text in the SELECT so it round-trips as the string 'Infinity' (the nearby code already special-cases some float8 paths this way)","Fix the producing expression (guard divide-by-zero, use NULLIF) so non-finite values never appear"],"exampleFix":"// before\nSELECT rate FROM metrics;\n// after\nSELECT CASE WHEN rate::text ~ '^(NaN|-?Infinity)$' THEN NULL ELSE rate END AS rate FROM metrics;","handlingStrategy":"fallback","validationCode":"-- run before converting results, or as the SELECT itself:\n-- SELECT CASE WHEN col::text ~ '^(NaN|-?Infinity)$' THEN NULL ELSE col END AS col FROM t\nfunction assertFiniteFloats(rows) {\n  for (const row of rows) {\n    for (const [k, v] of Object.entries(row)) {\n      if (typeof v === \"number\" && !Number.isFinite(v)) {\n        throw new Error(`column ${k} contains a non-finite float that cannot be JSON-serialized`);\n      }\n    }\n  }\n}","typeGuard":"const isFiniteNumber = (v) => typeof v === \"number\" && Number.isFinite(v);","tryCatchPattern":"try {\n  const result = await runPgStep(sql);\n  return result;\n} catch (e) {\n  if (String(e.message).includes(\"invalid json-float\")) {\n    // retry with non-finite floats coerced to NULL in the query\n    const safeSql = sql.replace(/\\bFROM\\b/i, \", 1 FROM\"); // or pre-arranged NULL-safe variant\n    return runPgStep(nullSafeSql);\n  }\n  throw e;\n}","preventionTips":["Guard producing expressions with NULLIF to avoid divide-by-zero Infinity","Coerce NaN/Inf columns to NULL or text in the SELECT","Validate at write time that float columns never store 'Infinity'/'NaN' sentinels","When sentinels are intentional, select the column as text so it survives JSON"],"tags":["postgresql","json","float","serialization"],"backgroundTag":"non-finite-float-json","analyzedSha":"e474e8803ce2ff5c2df09a58dab51d45f5c922ca","analyzedAt":"2026-09-03T12:38:19.024Z","contentChangedAt":"2026-09-03T12:38:19.024Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}