GitoxideLabs/gitoxide · error

Couldn't parse span from

Error message

Couldn't parse span from '{period} {count}'

What it means

In gix-date's relative-date `subtract_pairs`, each (count, unit) pair must be convertible into a jiff `Span`. The closure `err` builds Error 'Couldn't parse span from \'{period} {count}\'' raised when `count.checked_mul(factor)` overflows for `Unit::Seconds(factor)` (and analogous failures building spans for other units), i.e. the numeric count times its factor does not fit.

Solutions

  1. Reduce the count to a value that fits when multiplied by the unit factor (keep counts within i64-safe bounds)
  2. Pre-validate the numeric count before handing the pair list to gix-date
  3. Catch the Error and report the offending 'period count' text to the user

Example fix

// before
parse(now, "18446744073709551615 seconds ago")
// after
let count = count.min(i64::MAX as u64 / factor);
parse(now, &format!("{count} seconds ago"))
Defensive patterns

Strategy: validation

Validate before calling

fn count_safe(count: u64, factor: u64) -> bool {
    count.checked_mul(factor).is_some()
}

Try / catch

match parse_relative(input) {
    Ok(t) => t,
    Err(e) => { warn!("bad relative date: {e}"); now() },
}

Prevention

When it happens

Trigger: Calling relative-date `parse` (directly or via `gix_date::parse`) with a pair whose count is huge — e.g. '18446744073709551615 seconds ago' — where `checked_mul` on the second-factor overflows i64/u64.

Common situations: Users passing absurdly large numbers to relative date expressions (`--since=999999999999999999999 seconds ago`) in tooling built on gix-date.

Understand the failure class

Background: "invalid duration" / "failed to parse duration": why your timeout, interval, or TTL string is rejected and which formats each library accepts — this error's family across 32 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/28dcb51b57aefc05. Report an issue: GitHub.

Appendix: source

Thrown at gix-date/src/parse/relative.rs:184

        /// Turn the fields back into a point in time: a day beyond the end of the month rolls over into the
        /// following month. One month before May 31st is thus May 1st, a day after April 30th.
        fn normalize(&self) -> Result<Zoned, Exn<Error>> {
            let first_of_month = civil::Date::new(self.year, self.month, 1)
                .or_raise(|| Error::new(format!("Date lies out of range: {}-{:02}", self.year, self.month)))?;
            let days_beyond_first = SignedDuration::from_secs((i64::from(self.day) - 1) * 24 * 60 * 60);
            first_of_month
                .checked_add(days_beyond_first)
                .or_raise(|| Error::new(format!("Day {} lies out of range", self.day)))?
                .to_datetime(self.time)
                .to_zoned(self.timezone.clone())
                .or_raise(|| Error::new("Could not convert date to a point in time"))
        }
    }

    let now = now.ok_or(ValidationError::new("Missing current time"))?;
    let mut fields = Fields::from(now);
    for Pair { period, count, unit } in pairs {
        let err = || Error::new(format!("Couldn't parse span from '{period} {count}'"));
        match unit {
            Unit::Seconds(factor) => {
                let seconds = count
                    .checked_mul(*factor)
                    .map(SignedDuration::from_secs)
                    .ok_or_else(err)?;
                let ts = fields.normalize()?.timestamp().checked_sub(seconds).or_raise(err)?;
                fields = ts.to_zoned(fields.timezone.clone()).into();
            }
            Unit::Months(factor) => {
                let months = count.checked_mul(*factor).ok_or_else(err)?;
                fields = fields.normalize()?.into();
                let total = (i64::from(fields.year) * 12 + i64::from(fields.month) - 1)
                    .checked_sub(months)
                    .ok_or_else(err)?;
                fields.year = i16::try_from(total.div_euclid(12)).ok().ok_or_else(err)?;
                fields.month = i8::try_from(total.rem_euclid(12) + 1).expect("a value in 1..=12");
            }

View on GitHub (pinned to e73179060b)