use std::fmt;
use std::time::Duration;
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
pub mod codes {
pub const NETWORK_ERROR: &str = "network_error";
pub const TIMEOUT: &str = "timeout";
pub const UNEXPECTED_RESPONSE: &str = "unexpected_response";
pub const EXTRACTION_TIMEOUT: &str = "extraction_timeout";
pub const INVALID_SIGNATURE: &str = "invalid_signature";
pub const SIGNATURE_TOO_OLD: &str = "signature_too_old";
pub const INVALID_PAYLOAD: &str = "invalid_payload";
pub const INVALID_ARGUMENT: &str = "invalid_argument";
}
#[derive(thiserror::Error)]
#[error(transparent)]
pub struct Error(Box<Inner>);
#[derive(Debug, thiserror::Error)]
#[error("{message} ({}{})", describe(.status, .code), request_suffix(.request_id))]
struct Inner {
status: u16,
code: String,
message: String,
request_id: Option<String>,
body: Option<String>,
retry_after: Option<Duration>,
#[source]
source: Option<BoxError>,
}
fn describe(status: &u16, code: &str) -> String {
if *status == 0 {
code.to_string()
} else {
format!("status {status}, {code}")
}
}
fn request_suffix(request_id: &Option<String>) -> String {
match request_id {
Some(id) => format!(", request {id}"),
None => String::new(),
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl Error {
pub fn status(&self) -> u16 {
self.0.status
}
pub fn code(&self) -> &str {
&self.0.code
}
pub fn message(&self) -> &str {
&self.0.message
}
pub fn request_id(&self) -> Option<&str> {
self.0.request_id.as_deref()
}
pub fn body(&self) -> Option<&str> {
self.0.body.as_deref()
}
pub fn retry_after(&self) -> Option<Duration> {
self.0.retry_after
}
pub fn is_auth_error(&self) -> bool {
self.0.status == 401
}
pub fn is_permission_error(&self) -> bool {
self.0.status == 403
}
pub fn is_not_found(&self) -> bool {
self.0.status == 404
}
pub fn is_rate_limited(&self) -> bool {
self.0.status == 429
}
pub fn is_timeout(&self) -> bool {
self.0.code == codes::TIMEOUT
}
pub fn is_retryable(&self) -> bool {
self.0.status == 0 || self.0.status == 429 || self.0.status >= 500
}
pub(crate) fn new(status: u16, code: &str, message: impl Into<String>) -> Self {
Error(Box::new(Inner {
status,
code: code.to_string(),
message: message.into(),
request_id: None,
body: None,
retry_after: None,
source: None,
}))
}
pub(crate) fn invalid_argument(message: impl Into<String>) -> Self {
Error::new(0, codes::INVALID_ARGUMENT, message)
}
pub(crate) fn with_source(mut self, source: impl Into<BoxError>) -> Self {
self.0.source = Some(source.into());
self
}
pub(crate) fn from_response(
status: u16,
request_id: Option<String>,
retry_after: Option<Duration>,
body: &[u8],
) -> Self {
let fallback = format!("Request failed with status {status}");
let envelope = serde_json::from_slice::<serde_json::Value>(body).ok();
let detail = envelope
.as_ref()
.and_then(|v| v.get("error"))
.and_then(|e| e.as_object());
let (code, message) = match detail {
Some(detail) => {
let code = detail.get("code").and_then(|c| c.as_str());
let message = detail.get("message").and_then(|m| m.as_str());
(
code.unwrap_or(codes::UNEXPECTED_RESPONSE).to_string(),
message.map_or(fallback, str::to_string),
)
}
None => (codes::UNEXPECTED_RESPONSE.to_string(), fallback),
};
Error(Box::new(Inner {
status,
code,
message,
request_id,
body: (!body.is_empty()).then(|| String::from_utf8_lossy(body).into_owned()),
retry_after,
source: None,
}))
}
pub(crate) fn transport(err: reqwest::Error) -> Self {
let (code, message) = if err.is_timeout() {
(codes::TIMEOUT, "The request timed out".to_string())
} else {
(
codes::NETWORK_ERROR,
format!("The request failed before a response arrived: {err}"),
)
};
Error::new(0, code, message).with_source(err)
}
pub(crate) fn decode(
status: u16,
request_id: Option<String>,
body: &[u8],
err: serde_json::Error,
) -> Self {
let mut e = Error::new(
status,
codes::UNEXPECTED_RESPONSE,
format!("The response could not be read: {err}"),
)
.with_source(err);
e.0.request_id = request_id;
e.0.body = (!body.is_empty()).then(|| String::from_utf8_lossy(body).into_owned());
e
}
}
pub type Result<T, E = Error> = std::result::Result<T, E>;