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}