Skip to main content

honk_me/
error.rs

1use std::fmt;
2use std::time::Duration;
3
4/// A `Result` whose error is [`honk_me::Error`](Error).
5pub type Result<T, E = Error> = std::result::Result<T, E>;
6
7/// Everything that can go wrong. Every variant except
8/// [`InvalidConfiguration`](Error::InvalidConfiguration) carries a [`Failure`] with the details
9/// (status, API error code, field errors, the idempotency key that was used, attempts, …).
10///
11/// ```
12/// # async fn demo(honk: honk_me::Honk, msg: honk_me::Message) {
13/// use honk_me::Error;
14///
15/// match honk.send(&msg, "order-1042-failed").await {
16///     Ok(accepted) => println!("stored as {}", accepted.id),
17///     Err(Error::Validation(f)) => eprintln!("a bug, don't retry: {:?}", f.fields),
18///     Err(e) if e.is_retryable() => {
19///         // Queue it and retry later with the same key: Honk deduplicates.
20///         let (key, after) = (e.idempotency_key(), e.retry_after());
21///         # let _ = (key, after);
22///     }
23///     Err(e) => eprintln!("{e}"),
24/// }
25/// # }
26/// ```
27#[derive(Debug)]
28#[non_exhaustive]
29pub enum Error {
30    /// The client is misconfigured: a missing or malformed URL or key (from
31    /// [`Honk::new`](crate::Honk::new), the builder or `from_env`).
32    InvalidConfiguration(String),
33    /// The message is invalid: rejected locally (`failure.local`) or by the server (400, 413,
34    /// 415, 422). [`Failure::fields`] lists every problem. Fix the message; don't retry.
35    Validation(Box<Failure>),
36    /// 401 or 403: `invalid_key`, `priority_not_allowed` (urgent without `allow_urgent`),
37    /// `project_suspended` or `workspace_suspended`.
38    Auth(Box<Failure>),
39    /// 429 after retries: `quota_exceeded` (daily, resets at UTC midnight), `rate_limited` or
40    /// `overloaded`. See [`Failure::retry_after`].
41    Quota(Box<Failure>),
42    /// 409 `idempotency_conflict`: the key was already used with a different payload in the
43    /// last 24 hours. Use a new key, or send the original payload.
44    Conflict(Box<Failure>),
45    /// Honk could not be reached on any attempt before the deadline.
46    Network(Box<Failure>),
47    /// The attempts timed out. The event may or may not have been stored; retrying later with
48    /// the same idempotency key is safe.
49    Timeout(Box<Failure>),
50    /// 5xx on every attempt before the deadline.
51    Server(Box<Failure>),
52    /// Any other unexpected answer: 404 (wrong URL), a redirect (never followed), a 2xx without
53    /// a message id.
54    Http(Box<Failure>),
55}
56
57/// The details of a failed send.
58#[derive(Debug, Default)]
59#[non_exhaustive]
60pub struct Failure {
61    /// The HTTP status, or `None` when no answer was received.
62    pub status: Option<u16>,
63    /// The API error code (`invalid_key`, `quota_exceeded`, `validation_failed`, …), or
64    /// `network_error` / `timeout`.
65    pub code: String,
66    /// A human-readable explanation (English).
67    pub message: String,
68    /// Every invalid field, for [`Error::Validation`].
69    pub fields: Vec<FieldError>,
70    /// `true` when the SDK rejected the message before sending anything.
71    pub local: bool,
72    /// The server's request id (`req_…`), when it answered.
73    pub request_id: Option<String>,
74    /// The `Idempotency-Key` that was used. Retry later with the same key to stay
75    /// duplicate-free.
76    pub idempotency_key: Option<String>,
77    /// The number of HTTP attempts made (0 when rejected locally).
78    pub attempts: u32,
79    /// The server's `Retry-After`, when present.
80    pub retry_after: Option<Duration>,
81    pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
82}
83
84/// One invalid field, from the server (`error.fields[]`) or local validation.
85#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
86#[non_exhaustive]
87pub struct FieldError {
88    /// The wire name: `message`, `group_key`, `metadata.region`, `Idempotency-Key`, `body`, …
89    pub field: String,
90    /// `required`, `too_long`, `too_short`, `invalid_enum`, `invalid_format`, `out_of_range`,
91    /// `not_allowed`, `invalid_utf8` or `requires_group_key`.
92    pub code: String,
93    /// A human-readable explanation.
94    #[serde(default)]
95    pub message: String,
96}
97
98impl FieldError {
99    pub(crate) fn new(field: impl Into<String>, code: &str, message: impl Into<String>) -> Self {
100        FieldError {
101            field: field.into(),
102            code: code.to_owned(),
103            message: message.into(),
104        }
105    }
106}
107
108impl Error {
109    /// The details of the failure (`None` for [`Error::InvalidConfiguration`]).
110    pub fn failure(&self) -> Option<&Failure> {
111        match self {
112            Error::InvalidConfiguration(_) => None,
113            Error::Validation(f)
114            | Error::Auth(f)
115            | Error::Quota(f)
116            | Error::Conflict(f)
117            | Error::Network(f)
118            | Error::Timeout(f)
119            | Error::Server(f)
120            | Error::Http(f) => Some(&**f),
121        }
122    }
123
124    /// Whether sending the same event again later (with the same idempotency key) may succeed:
125    /// network errors, timeouts, 429 and 5xx.
126    pub fn is_retryable(&self) -> bool {
127        matches!(
128            self,
129            Error::Network(_) | Error::Timeout(_) | Error::Server(_) | Error::Quota(_)
130        )
131    }
132
133    /// The HTTP status, when the server answered.
134    pub fn status(&self) -> Option<u16> {
135        self.failure().and_then(|f| f.status)
136    }
137
138    /// The API error code (`invalid_key`, `quota_exceeded`, …), when there is one.
139    pub fn code(&self) -> Option<&str> {
140        self.failure()
141            .map(|f| f.code.as_str())
142            .filter(|c| !c.is_empty())
143    }
144
145    /// Every invalid field (empty unless [`Error::Validation`]).
146    pub fn fields(&self) -> &[FieldError] {
147        self.failure().map_or(&[], |f| f.fields.as_slice())
148    }
149
150    /// The idempotency key that was used, to retry the same event later.
151    pub fn idempotency_key(&self) -> Option<&str> {
152        self.failure().and_then(|f| f.idempotency_key.as_deref())
153    }
154
155    /// The server's `Retry-After`, when present.
156    pub fn retry_after(&self) -> Option<Duration> {
157        self.failure().and_then(|f| f.retry_after)
158    }
159
160    pub(crate) fn local_validation(fields: Vec<FieldError>) -> Error {
161        Error::Validation(Box::new(Failure {
162            code: "validation_failed".into(),
163            message: "invalid message".into(),
164            fields,
165            local: true,
166            ..Failure::default()
167        }))
168    }
169}
170
171impl fmt::Display for Error {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        let failure = match self {
174            Error::InvalidConfiguration(msg) => return write!(f, "honk: {msg}"),
175            _ => self.failure().expect("every other variant has a failure"),
176        };
177        f.write_str("honk: ")?;
178        if let Some(status) = failure.status {
179            write!(f, "{status} ")?;
180            if !failure.code.is_empty() {
181                write!(f, "{}: ", failure.code)?;
182            }
183        }
184        f.write_str(&failure.message)?;
185        if !failure.fields.is_empty() {
186            let parts: Vec<String> = failure
187                .fields
188                .iter()
189                .map(|e| {
190                    format!(
191                        "{} {}",
192                        e.field,
193                        if e.message.is_empty() {
194                            &e.code
195                        } else {
196                            &e.message
197                        }
198                    )
199                })
200                .collect();
201            write!(f, " ({})", parts.join("; "))?;
202        }
203        Ok(())
204    }
205}
206
207impl std::error::Error for Error {
208    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
209        self.failure()
210            .and_then(|f| f.source.as_deref())
211            .map(|e| e as &(dyn std::error::Error + 'static))
212    }
213}