nikivdev/code · error

failed to resolve local midnight

Error message

failed to resolve local midnight

What it means

next_local_midnight_utc converts the naive local midnight into a concrete local DateTime. When a DST transition makes midnight ambiguous or skipped, from_local_datetime().single() and .earliest() can both return None, and the function then throws 'failed to resolve local midnight' because no valid local instant corresponds to tomorrow at 00:00.

Source

Thrown at src/ssh_keys.rs:526

fn unlock_expires_at(entry: &SshKeyUnlock) -> Option<DateTime<Utc>> {
    DateTime::<Utc>::from_timestamp(entry.expires_at, 0)
}

fn next_local_midnight_utc() -> Result<DateTime<Utc>> {
    let now = Local::now();
    let tomorrow = now
        .date_naive()
        .succ_opt()
        .ok_or_else(|| anyhow::anyhow!("failed to calculate next day"))?;
    let naive = tomorrow
        .and_hms_opt(0, 0, 0)
        .ok_or_else(|| anyhow::anyhow!("failed to build midnight time"))?;
    let local_dt = Local
        .from_local_datetime(&naive)
        .single()
        .or_else(|| Local.from_local_datetime(&naive).earliest())
        .ok_or_else(|| anyhow::anyhow!("failed to resolve local midnight"))?;
    Ok(local_dt.with_timezone(&Utc))
}

fn prompt_touch_id() -> Result<()> {
    if !cfg!(target_os = "macos") {
        bail!("Touch ID is not available on this OS");
    }
    if std::env::var("FLOW_NO_TOUCH_ID").is_ok() || !std::io::stdin().is_terminal() {
        bail!("Touch ID prompt requires an interactive terminal");
    }

    let reason = "Flow needs Touch ID to unlock SSH keys.";
    let reason = reason.replace('\\', "\\\\").replace('"', "\\\"");
    let script = format!(
        r#"ObjC.import('stdlib');
ObjC.import('Foundation');
ObjC.import('LocalAuthentication');
const context = $.LAContext.alloc.init;

View on GitHub (pinned to a747e741ae)

Solutions

  1. Set the system timezone to one without midnight DST transitions, or update the tz database
  2. In code, fall back to the latest() resolution or noon instead of midnight when single()/earliest() are None
  3. Use Local.with_ymd_and_hms and check LocalResult variants explicitly, handling None with a +1h offset

Example fix

// before
.ok_or_else(|| anyhow::anyhow!("failed to resolve local midnight"))?;
// after
.single()
.or_else(|| Local.from_local_datetime(&naive).earliest())
.or_else(|| Local.from_local_datetime(&naive).latest())
.ok_or_else(|| anyhow::anyhow!("failed to resolve local midnight"))?;
Defensive patterns

Strategy: fallback

Validate before calling

use chrono::TimeZone;
let naive = chrono::Local::now().date_naive().succ_opt()?.and_hms_opt(0, 0, 0)?;
let res = chrono::Local.from_local_datetime(&naive);
let ok = matches!(res, chrono::LocalResult::Single(_) | chrono::LocalResult::Ambiguous(_, _));
if !ok { eprintln!("local midnight does not exist tomorrow (DST gap); expiry will fall back"); }

Try / catch

match unlock_ssh_key() {
    Ok(expiry) => expiry,
    Err(e) if e.to_string().contains("failed to resolve local midnight") => {
        Utc::now() + chrono::Duration::hours(24) // fallback expiry
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling next_local_midnight_utc (via unlock_ssh_key) when tomorrow at 00:00 local time falls inside a DST spring-forward gap in the machine's timezone (midnight does not exist that day), and even the earliest() resolution fails.

Common situations: Running on a machine whose timezone has DST transitions at midnight (e.g. some historical/observance timezones like America/Sao_Paulo before 2019, or Asia/Beirut), with the system clock near such a transition.

Related errors


AI-assisted analysis of nikivdev/code@a747e741ae (2026-09-01). Data as JSON: /api/errors/5873b4fa55dc3b2d. Report an issue: GitHub.