Notificaciones push desde Rust

El crate honk-honk es un cliente asíncrono sobre Tokio y reqwest con rustls, así que no necesita OpenSSL. La feature blocking añade un cliente síncrono para scripts y CLI.

Paquete
honk-honk Publicado
Código fuente
github.com/honk-hq/rust · Licencia MIT
Requisitos
Rust 1.85 o posterior.

Instalación

Para código síncrono: cargo add honk-honk --features blocking.

Terminal
cargo add honk-honk
cargo add tokio --features macros,rt-multi-thread   # if you don't have a runtime yet

Quien tenga una clave de ingesta (honk_…) puede enviar mensajes a su proyecto. Guárdala en servidores, tareas y secretos de CI, nunca en un navegador ni en una app móvil o de escritorio.

Enviar un evento

O de forma explícita: Honk::new("https://honk-honk.app", key)?. Clonar Honk es barato: crea uno y compártelo, para que las conexiones sigan abiertas.

use honk::Honk;

#[tokio::main]
async fn main() -> honk::Result<()> {
    let honk = Honk::from_env()?; // HONK_URL, HONK_KEY (+ HONK_SOURCE, HONK_ENVIRONMENT, HONK_CHANNEL)
    honk.beep("Backup finished", "nightly pg_dump took 42 s").await?;
    Ok(())
}

Receta: una solicitud de cliente en tu teléfono

Un grupo por solicitud y tu propia clave de idempotencia, para que un webhook reintentado nunca suene dos veces.

use honk::{Category, Message, Priority, Severity};

let msg = Message::new(format!("{} asked for a quote: {}", req.name, req.summary))
    .title("New quote request")
    .severity(Severity::LOUD)
    .priority(Priority::High)
    .category(Category::Customers)
    .group_key(format!("requests/{}", req.id))
    .url(format!("https://shop.example.com/admin/requests/{}", req.id)); // https only, "Open link"
honk.honk(&msg, format!("request-{}", req.id).as_str()).await?;

La escala Honk y las incidencias

Cada helper devuelve un PendingSend: encadena más campos y después haz .await.

use honk::Priority;

honk.loud("Disk 91% full", "db-1 /var is at 91%")
    .group_key("disk/db-1/var")
    .priority(Priority::High)
    .metadata("used_percent", 91)
    .await?;

honk.uhoh("db/backup", "Backup failed", "pg_dump exited with 1").await?;                             // a long honk by default
honk.phew("db/backup", "Backup is back", "pg_dump finished in 41 s. Nobody needs to know.").await?;  // a beep by default

El cliente bloqueante

Ejecuta el cliente asíncrono en un runtime privado de un solo hilo, con los mismos reintentos. No lo llames desde dentro de un runtime asíncrono.

// Cargo.toml: honk-honk = { version = "0.4", features = ["blocking"] }
use honk::blocking::Honk;

let honk = Honk::from_env()?;
honk.beep("Backup finished", "nightly pg_dump took 42 s").send()?;
honk.honk(&honk::Message::new("Imported 1 204 rows"), None)?;

Errores

Cada variante de Error lleva un Failure con el estado, el código, cada campo no válido y retry_after.

use honk::Error;

match honk.honk(&msg, "order-1042-failed").await {
    Ok(_) => {}
    Err(Error::Validation(f)) => tracing::error!(?f.fields, "a bug: don't retry"),
    Err(e) if e.is_retryable() => queue.retry_later(e.idempotency_key(), e.retry_after()),
    Err(e) => return Err(e.into()),
}

Reintentos que nunca envían dos veces

  • Cada envío lleva una Idempotency-Key: la tuya o un UUIDv7 nuevo. La misma clave se reutiliza en cada reintento y, durante 24 horas, Honk responde a una repetición con el id original y duplicate: true, así que una respuesta perdida nunca crea un segundo mensaje.
  • Solo se reintentan los errores de red, los tiempos de espera agotados, 429 y 5xx, con backoff exponencial y jitter completo, y nunca antes del Retry-After del servidor. Las demás respuestas 4xx no se reintentan nunca: hay que corregir la solicitud.
  • Cada intento se corta a los 5 segundos y todo se detiene a los 30. Una espera que superaría ese plazo, como una cuota diaria que se reinicia a medianoche, falla al momento y te dice cuándo reintentar.
  • Los campos se validan antes de enviar, y todos los errores de validación se devuelven juntos.