fix(schedules): use typed error for pause/resume 404 discrimination

Replace brittle msg.contains("not found") string matching in
post_schedule_pause / post_schedule_resume with a typed
ScheduleNotFoundOrCancelled error that handlers downcast on directly.

pause() and resume() now return Err(ScheduleNotFoundOrCancelled(id).into())
instead of bail!("schedule {id} not found or is cancelled"); handlers call
e.downcast_ref::<ScheduleNotFoundOrCancelled>().is_some() for the 404 branch,
making the discrimination stable even if the error message wording changes.
This commit is contained in:
iris 2026-06-27 13:57:50 +02:00 committed by mara
commit dbd4b7a15c
2 changed files with 28 additions and 12 deletions

View file

@ -19,6 +19,20 @@ use anyhow::{Context, Result, bail};
use rusqlite::{Connection, OptionalExtension, params};
use serde::{Deserialize, Serialize};
/// Typed error returned by [`ScheduledPrompts::pause`] and
/// [`ScheduledPrompts::resume`] when the target row does not exist or
/// is already cancelled. Handlers downcast on this type to emit 404
/// rather than 500, avoiding brittle string-matching on the message.
#[derive(Debug)]
pub struct ScheduleNotFoundOrCancelled(pub i64);
impl std::fmt::Display for ScheduleNotFoundOrCancelled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "schedule {} not found or is cancelled", self.0)
}
}
impl std::error::Error for ScheduleNotFoundOrCancelled {}
const SCHEMA: &str = r"
CREATE TABLE IF NOT EXISTS scheduled_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -577,7 +591,8 @@ impl ScheduledPrompts {
/// `next_fire_at_unix` is preserved so the first resume fires at
/// the next intended cadence instant (no catch-up needed — a
/// paused schedule simply slips its upcoming fire). Returns
/// `Err` when the schedule is cancelled or does not exist.
/// `Err(ScheduleNotFoundOrCancelled)` when the schedule is
/// cancelled or does not exist (handlers downcast to emit 404).
pub fn pause(&self, id: i64) -> Result<()> {
let conn = self.conn.lock().unwrap();
let now = now_unix();
@ -588,13 +603,14 @@ impl ScheduledPrompts {
params![now, id],
)?;
if n == 0 {
bail!("schedule {id} not found or is cancelled");
return Err(ScheduleNotFoundOrCancelled(id).into());
}
Ok(())
}
/// Resume a paused schedule. Idempotent; no-op on an active row.
/// Returns `Err` when the schedule is cancelled or does not exist.
/// Returns `Err(ScheduleNotFoundOrCancelled)` when the schedule is
/// cancelled or does not exist (handlers downcast to emit 404).
pub fn resume(&self, id: i64) -> Result<()> {
let conn = self.conn.lock().unwrap();
let n = conn.execute(
@ -604,7 +620,7 @@ impl ScheduledPrompts {
params![id],
)?;
if n == 0 {
bail!("schedule {id} not found or is cancelled");
return Err(ScheduleNotFoundOrCancelled(id).into());
}
Ok(())
}