use std::collections::BTreeMap;
use std::fmt;
use std::time::{Duration, SystemTime};
use serde_json::Value;
use crate::transport::TransportError;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug)]
pub struct ApiError {
pub r#type: String,
pub message: String,
pub status: Option<u16>,
pub request_id: Option<String>,
pub errors: BTreeMap<String, Vec<String>>,
pub retry_after: Option<f64>,
pub raw: Option<Value>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Validation(ApiError),
Authentication(ApiError),
Permission(ApiError),
NotFound(ApiError),
Conflict(ApiError),
IdempotencyMismatch(ApiError),
RateLimit(ApiError),
PayloadTooLarge(ApiError),
Api(ApiError),
Connection { message: String },
Serialization(String),
Config(String),
}
impl Error {
pub fn api(&self) -> Option<&ApiError> {
match self {
Error::Validation(e)
| Error::Authentication(e)
| Error::Permission(e)
| Error::NotFound(e)
| Error::Conflict(e)
| Error::IdempotencyMismatch(e)
| Error::RateLimit(e)
| Error::PayloadTooLarge(e)
| Error::Api(e) => Some(e),
_ => None,
}
}
pub fn error_type(&self) -> Option<&str> {
match self {
Error::Connection { .. } => Some("connection_error"),
_ => self.api().map(|e| e.r#type.as_str()),
}
}
pub fn status(&self) -> Option<u16> {
self.api().and_then(|e| e.status)
}
pub fn request_id(&self) -> Option<&str> {
self.api().and_then(|e| e.request_id.as_deref())
}
pub fn validation_errors(&self) -> Option<&BTreeMap<String, Vec<String>>> {
match self {
Error::Validation(e) => Some(&e.errors),
_ => None,
}
}
pub fn retry_after(&self) -> Option<f64> {
match self {
Error::RateLimit(e) => e.retry_after,
_ => None,
}
}
pub(crate) fn connection(err: TransportError) -> Self {
Error::Connection {
message: format!("Could not reach Anypost: {}", err.message),
}
}
pub(crate) fn from_response(status: u16, body: &[u8], headers: &[(String, String)]) -> Self {
let value: Value = serde_json::from_slice(body).unwrap_or(Value::Null);
let request_id = read_request_id(headers);
let mut type_: Option<String> = None;
let mut message: Option<String> = None;
let mut errors: BTreeMap<String, Vec<String>> = BTreeMap::new();
match value.get("error") {
Some(Value::Object(obj)) => {
type_ = obj.get("type").and_then(Value::as_str).map(str::to_string);
message = obj
.get("message")
.and_then(Value::as_str)
.map(str::to_string);
if let Some(Value::Object(map)) = obj.get("errors") {
for (key, val) in map {
if let Value::Array(items) = val {
errors.insert(
key.clone(),
items
.iter()
.filter_map(Value::as_str)
.map(str::to_string)
.collect(),
);
}
}
}
}
Some(Value::String(code)) => {
type_ = Some(code.clone());
message = value
.get("message")
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| Some(code.replace('_', " ")));
}
_ => {}
}
let type_ = type_.unwrap_or_else(|| type_from_status(status).to_string());
let message = message.unwrap_or_else(|| default_message(status));
let retry_after = retry_after_seconds(headers);
let api = ApiError {
r#type: type_,
message,
status: Some(status),
request_id,
errors,
retry_after,
raw: Some(value),
};
build(status, api)
}
}
fn build(status: u16, api: ApiError) -> Error {
match api.r#type.as_str() {
"validation_error" => Error::Validation(api),
"authentication_error" => Error::Authentication(api),
"permission_error" => Error::Permission(api),
"not_found" => Error::NotFound(api),
"conflict" | "idempotency_concurrent" | "webhook_rotation_in_progress" => {
Error::Conflict(api)
}
"idempotency_mismatch" => Error::IdempotencyMismatch(api),
"rate_limit_exceeded" => Error::RateLimit(api),
"payload_too_large" => Error::PayloadTooLarge(api),
"provisioning_error" | "internal_error" => Error::Api(api),
_ => by_status(status, api),
}
}
fn by_status(status: u16, api: ApiError) -> Error {
match status {
401 => Error::Authentication(api),
403 => Error::Permission(api),
404 => Error::NotFound(api),
409 => Error::Conflict(api),
413 => Error::PayloadTooLarge(api),
429 => Error::RateLimit(api),
400 | 422 => Error::Validation(api),
_ => Error::Api(api),
}
}
fn type_from_status(status: u16) -> &'static str {
match status {
400 | 422 => "validation_error",
401 => "authentication_error",
403 => "permission_error",
404 => "not_found",
409 => "conflict",
413 => "payload_too_large",
429 => "rate_limit_exceeded",
s if s >= 500 => "internal_error",
_ => "api_error",
}
}
fn default_message(status: u16) -> String {
format!("Anypost request failed with status {status}.")
}
pub(crate) fn retry_after_seconds(headers: &[(String, String)]) -> Option<f64> {
let value = header(headers, "retry-after")?;
let value = value.trim();
if value.is_empty() {
return None;
}
if let Ok(seconds) = value.parse::<f64>() {
return Some(seconds.max(0.0));
}
if let Ok(when) = httpdate::parse_http_date(value) {
let delta = when
.duration_since(SystemTime::now())
.unwrap_or(Duration::ZERO);
return Some(delta.as_secs_f64());
}
None
}
const REQUEST_ID_HEADERS: [&str; 3] =
["anypost-request-id", "x-anypost-request-id", "x-request-id"];
pub(crate) fn read_request_id(headers: &[(String, String)]) -> Option<String> {
for name in REQUEST_ID_HEADERS {
if let Some(value) = header(headers, name) {
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
fn header<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a str> {
headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Connection { message } => write!(f, "{message}"),
Error::Serialization(message) => {
write!(f, "could not serialize request body: {message}")
}
Error::Config(message) => write!(f, "{message}"),
_ => {
let api = self.api().expect("HTTP variants carry an ApiError");
write!(f, "{} ({})", api.message, api.r#type)
}
}
}
}
impl std::error::Error for Error {}