windmill-labs/windmill · error

invalid json-float

Error message

invalid json-float

What it means

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.

Source

Thrown at backend/windmill-worker/src/pg_executor.rs:1812

    // JSON has no encoding for NaN / +Inf / -Inf, but Postgres `float4` /
    // `float8` (and `numeric`, via the special `'NaN'` value) do return them.
    // Pre-fix the worker errored with "invalid json-float", failing the
    // entire query. Round-trip these as JSON strings ("NaN", "Infinity",
    // "-Infinity") so the rest of the row still comes through; users who
    // need numeric semantics can filter them out client-side.
    let f64_to_json_number = |raw_val: f64| -> Result<JSONValue, Error> {
        if raw_val.is_nan() {
            return Ok(JSONValue::String("NaN".to_string()));
        }
        if raw_val.is_infinite() {
            return Ok(JSONValue::String(if raw_val > 0.0 {
                "Infinity".to_string()
            } else {
                "-Infinity".to_string()
            }));
        }
        let temp =
            serde_json::Number::from_f64(raw_val).ok_or(anyhow::anyhow!("invalid json-float"))?;
        Ok(JSONValue::Number(temp))
    };
    Ok(match *column.type_() {
        // for rust-postgres <> postgres type-mappings: https://docs.rs/postgres/latest/postgres/types/trait.FromSql.html#types
        // for postgres types: https://www.postgresql.org/docs/7.4/datatype.html#DATATYPE-TABLE

        // single types
        Type::BOOL => get_basic(row, column, column_i, |a: bool| Ok(JSONValue::Bool(a)))?,
        Type::BIT => get_basic(row, column, column_i, |a: bit_vec::BitVec| match a.len() {
            1 => Ok(JSONValue::Bool(a.get(0).unwrap())),
            _ => Ok(JSONValue::String(
                a.iter()
                    .map(|x| if x { "1" } else { "0" })
                    .collect::<String>(),
            )),
        })?,
        Type::INT2 => get_basic(row, column, column_i, |a: i16| {
            Ok(JSONValue::Number(serde_json::Number::from(a)))

View on GitHub (pinned to e474e8803c)

Solutions

  1. 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`
  2. Prevent NaN/Inf at write time by validating inputs before INSERT
  3. 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)
  4. Fix the producing expression (guard divide-by-zero, use NULLIF) so non-finite values never appear

Example fix

// before
SELECT rate FROM metrics;
// after
SELECT CASE WHEN rate::text ~ '^(NaN|-?Infinity)$' THEN NULL ELSE rate END AS rate FROM metrics;
Defensive patterns

Strategy: fallback

Validate before calling

-- run before converting results, or as the SELECT itself:
-- SELECT CASE WHEN col::text ~ '^(NaN|-?Infinity)$' THEN NULL ELSE col END AS col FROM t
function assertFiniteFloats(rows) {
  for (const row of rows) {
    for (const [k, v] of Object.entries(row)) {
      if (typeof v === "number" && !Number.isFinite(v)) {
        throw new Error(`column ${k} contains a non-finite float that cannot be JSON-serialized`);
      }
    }
  }
}

Type guard

const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);

Try / catch

try {
  const result = await runPgStep(sql);
  return result;
} catch (e) {
  if (String(e.message).includes("invalid json-float")) {
    // retry with non-finite floats coerced to NULL in the query
    const safeSql = sql.replace(/\bFROM\b/i, ", 1 FROM"); // or pre-arranged NULL-safe variant
    return runPgStep(nullSafeSql);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/65d25e26b3e57728. Report an issue: GitHub.