windmill-labs/windmill · error

Invalid time value

Error message

Invalid time value

What it means

PostgreSQL TIMETZ values are decoded manually: microseconds since midnight plus a UTC offset are normalized to a seconds-of-day count, then chrono::NaiveTime::from_num_seconds_from_midnight_opt builds the time. That constructor returns None when the seconds value exceeds 86399 or the nanosecond value exceeds 1_999_999_999, and the code maps that to "Invalid time value". This happens only for corrupt/out-of-range wire data, since the modulo (utc_sec + 3600*24) % (3600*24) normally keeps seconds in range.

Source

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

    fn accepts(ty: &Type) -> bool {
        matches!(ty, &Type::INTERVAL)
    }
}

struct TimeTZStr(String);
impl<'a> FromSql<'a> for TimeTZStr {
    fn from_sql(
        _: &Type,
        mut raw: &'a [u8],
    ) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
        let microsecond = raw.get_i64();
        let offset = raw.get_i32();
        let utc_sec = (microsecond / 1_000_000) + offset as i64;
        let utc = chrono::NaiveTime::from_num_seconds_from_midnight_opt(
            ((utc_sec + 3600 * 24) % (3600 * 24)) as u32,
            ((microsecond % 1_000_000) * 1_000) as u32,
        )
        .ok_or_else(|| anyhow::anyhow!("Invalid time value"))?;
        // ISO-8601: append `+00:00` since TIMETZ is normalised to UTC here.
        Ok(TimeTZStr(format!("{}+00:00", utc)))
    }

    fn accepts(ty: &Type) -> bool {
        matches!(ty, &Type::TIMETZ)
    }
}

/// Format a `NaiveDateTime` as ISO-8601 (`YYYY-MM-DDTHH:MM:SS[.fff…]`).
/// chrono's default `to_string` uses a space separator, which is not parseable
/// by `new Date(s)` in older JS engines or Python's `datetime.fromisoformat`
/// before 3.11. Use the explicit format string so output is portable.
fn format_naive_datetime_iso(dt: &chrono::NaiveDateTime) -> String {
    if dt.and_utc().timestamp_subsec_nanos() == 0 {
        dt.format("%Y-%m-%dT%H:%M:%S").to_string()
    } else {
        dt.format("%Y-%m-%dT%H:%M:%S%.f").to_string()

View on GitHub (pinned to e474e8803c)

Solutions

  1. Inspect the offending value in Postgres (SELECT column, column::text FROM ... WHERE ...) and fix/normalize the stored TIMETZ.
  2. Cast the column to text in the query (SELECT tz_col::text) so the raw string is returned and the binary decode path is skipped.
  3. Cast to time (SELECT tz_col::time) to drop the timezone offset, which uses the well-tested NaiveTime path.
  4. If you control ingestion, store TIMESTAMPTZ instead of TIMETZ, which avoids this custom decoder entirely.

Example fix

-- before
SELECT meeting_at FROM schedules;  -- meeting_at is TIMETZ
-- after
SELECT meeting_at::text AS meeting_at FROM schedules;
Defensive patterns

Strategy: validation

Validate before calling

-- verify stored TIMETZ values are in the valid range before querying them from a job
SELECT id FROM t WHERE meeting_at < time '00:00:00+00' OR meeting_at > time '23:59:59.999999+00' OR meeting_at IS NOT NULL AND extract(epoch from meeting_at) NOT BETWEEN 0 AND 86399;

Try / catch

try {
  return await query('SELECT meeting_at FROM schedules');
} catch (e) {
  if (String(e.message).includes('Invalid time value')) {
    return await query('SELECT meeting_at::text AS meeting_at FROM schedules');
  }
  throw e;
}

Prevention

When it happens

Trigger: Decoding a TIMETZ column whose decoded microsecond field is negative or larger than a day's worth of microseconds in a way the normalization doesn't absorb (e.g. microsecond negative enough that (microsecond/1_000_000 + offset) before the modulo is < -86400), producing a negative seconds value that NaiveTime rejects.

Common situations: Extreme TIMETZ offsets combined with times near midnight; data written by non-Postgres tools or binary replication that encodes time-of-day unconventionally; a Postgres instance/extension emitting microsecond fields outside the documented range; decoding rows fetched by a Windmill PostgreSQL script.

Related errors


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