use std::collections::BTreeMap;
use std::fmt;
use std::time::Duration;
use reqwest::StatusCode;
use crate::ratelimit::Scope;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("HTTP transport error")]
Transport(#[source] reqwest::Error),
#[error("could not build request URL")]
Url(#[from] url::ParseError),
#[error("{method} {path} failed with HTTP {status}: {detail}")]
Api {
status: StatusCode,
method: reqwest::Method,
path: String,
detail: String,
#[source]
body: ApiError,
},
#[error("could not decode response body as {expected}")]
Decode {
expected: &'static str,
body: String,
#[source]
source: serde_json::Error,
},
#[error("could not encode request body")]
Encode(#[source] serde_json::Error),
#[error(
"local rate limit for scope {scope} would block for {wait:.1?}, over the limit of {max_wait:.1?}"
)]
RateLimitWouldBlock {
scope: Scope,
wait: Duration,
max_wait: Duration,
},
#[error("still rate limited after {attempts} attempts")]
RateLimited {
attempts: u32,
retry_after: Option<Duration>,
#[source]
body: ApiError,
},
#[error("{0}")]
Invalid(#[from] InvalidValue),
#[error("malformed Link header: {0}")]
MalformedLink(String),
}
impl Error {
pub(crate) fn transport(mut err: reqwest::Error) -> Self {
if let Some(url) = err.url_mut() {
url.set_query(None);
url.set_fragment(None);
}
Self::Transport(err)
}
pub fn status(&self) -> Option<StatusCode> {
match self {
Self::Api { status, .. } => Some(*status),
Self::RateLimited { .. } => Some(StatusCode::TOO_MANY_REQUESTS),
_ => None,
}
}
pub fn is_not_found(&self) -> bool {
self.status() == Some(StatusCode::NOT_FOUND)
}
pub fn is_unauthorized(&self) -> bool {
self.status() == Some(StatusCode::UNAUTHORIZED)
}
pub fn is_forbidden(&self) -> bool {
self.status() == Some(StatusCode::FORBIDDEN)
}
pub fn is_validation(&self) -> bool {
matches!(self, Self::Invalid(_)) || self.status() == Some(StatusCode::BAD_REQUEST)
}
pub fn is_rate_limited(&self) -> bool {
matches!(
self,
Self::RateLimited { .. } | Self::RateLimitWouldBlock { .. }
)
}
pub fn api_error(&self) -> Option<&ApiError> {
match self {
Self::Api { body, .. } | Self::RateLimited { body, .. } => Some(body),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid {field}: {reason} (got {value:?})")]
pub struct InvalidValue {
pub field: &'static str,
pub reason: &'static str,
pub value: String,
}
pub(crate) fn check_path_segment(field: &'static str, value: &str) -> Result<(), InvalidValue> {
if matches!(value, "" | "." | "..") {
return Err(InvalidValue::new(
field,
"is not addressable as a path segment",
value,
));
}
Ok(())
}
impl InvalidValue {
pub(crate) fn new(field: &'static str, reason: &'static str, value: impl Into<String>) -> Self {
Self {
field,
reason,
value: value.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub struct ApiError(pub ErrorDetail);
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.render(f, true)
}
}
impl ApiError {
pub fn parse(body: &str) -> Self {
match serde_json::from_str::<ErrorDetail>(body) {
Ok(detail) => Self(detail),
Err(_) if body.trim().is_empty() => Self(ErrorDetail::Message(String::new())),
Err(_) => Self(ErrorDetail::Message(truncate(body, 2048))),
}
}
pub fn detail(&self) -> Option<&str> {
match &self.0 {
ErrorDetail::Message(m) => Some(m),
ErrorDetail::Map(m) => match m.get("detail")? {
ErrorDetail::Message(m) => Some(m),
_ => None,
},
ErrorDetail::List(_) => None,
}
}
pub fn non_field_errors(&self) -> Vec<&str> {
self.field("non_field_errors")
.map(ErrorDetail::messages)
.unwrap_or_default()
}
pub fn field(&self, name: &str) -> Option<&ErrorDetail> {
match &self.0 {
ErrorDetail::Map(m) => m.get(name),
_ => None,
}
}
pub fn bulk_items(&self) -> Option<&[ErrorDetail]> {
match &self.0 {
ErrorDetail::List(items) => Some(items),
_ => None,
}
}
pub fn messages(&self) -> Vec<(String, &str)> {
let mut out = Vec::new();
self.0.walk(&mut String::new(), &mut out, true);
out
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum ErrorDetail {
Message(String),
List(Vec<ErrorDetail>),
Map(BTreeMap<String, ErrorDetail>),
}
impl ErrorDetail {
pub fn messages(&self) -> Vec<&str> {
match self {
Self::Message(m) => vec![m.as_str()],
Self::List(items) => items.iter().flat_map(Self::messages).collect(),
Self::Map(m) => m.values().flat_map(Self::messages).collect(),
}
}
fn walk<'a>(&'a self, path: &mut String, out: &mut Vec<(String, &'a str)>, index_list: bool) {
match self {
Self::Message(m) => out.push((path.clone(), m.as_str())),
Self::List(items) => {
for (i, item) in items.iter().enumerate() {
if index_list {
let restore = push_segment(path, &i.to_string());
item.walk(path, out, false);
path.truncate(restore);
} else {
item.walk(path, out, false);
}
}
}
Self::Map(map) => {
for (key, value) in map {
let restore = push_segment(path, key);
value.walk(path, out, false);
path.truncate(restore);
}
}
}
}
fn render(&self, f: &mut fmt::Formatter<'_>, index_list: bool) -> fmt::Result {
let mut messages = Vec::new();
self.walk(&mut String::new(), &mut messages, index_list);
match messages.as_slice() {
[] => f.write_str("(no detail)"),
[(path, msg)] if path.is_empty() || path == "detail" => f.write_str(msg),
_ => {
let mut first = true;
for (path, msg) in &messages {
if !first {
f.write_str("; ")?;
}
first = false;
if path.is_empty() {
f.write_str(msg)?;
} else {
write!(f, "{path}: {msg}")?;
}
}
Ok(())
}
}
}
}
impl fmt::Display for ErrorDetail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.render(f, false)
}
}
fn push_segment(path: &mut String, segment: &str) -> usize {
let restore = path.len();
if !path.is_empty() {
path.push('.');
}
path.push_str(segment);
restore
}
pub(crate) fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_owned();
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &s[..end])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_bare_detail_body() {
let err = ApiError::parse(r#"{"detail":"Not found."}"#);
assert_eq!(err.detail(), Some("Not found."));
assert_eq!(err.to_string(), "Not found.");
}
#[test]
fn parses_field_keyed_errors() {
let err = ApiError::parse(r#"{"ttl":["Ensure this value is greater than 3600."]}"#);
assert_eq!(
err.field("ttl").map(ErrorDetail::messages),
Some(vec!["Ensure this value is greater than 3600."])
);
}
#[test]
fn parses_nested_field_errors() {
let err = ApiError::parse(r#"{"captcha":{"solution":["Invalid captcha."]}}"#);
assert_eq!(
err.messages(),
vec![("captcha.solution".to_owned(), "Invalid captcha.")]
);
}
#[test]
fn parses_non_field_errors() {
let body = r#"{"non_field_errors":["Cannot create multiple default policies."]}"#;
let err = ApiError::parse(body);
assert_eq!(
err.non_field_errors(),
vec!["Cannot create multiple default policies."]
);
}
#[test]
fn keeps_bulk_item_positions() {
let err = ApiError::parse(r#"[{},{"records":["Invalid record."]},{}]"#);
let items = err.bulk_items().expect("body is an array");
assert_eq!(items.len(), 3);
assert_eq!(items[0].messages(), Vec::<&str>::new());
assert_eq!(items[1].messages(), vec!["Invalid record."]);
assert_eq!(
err.messages(),
vec![("1.records".to_owned(), "Invalid record.")]
);
}
#[test]
fn falls_back_to_raw_text_for_non_json() {
let err = ApiError::parse("<html>502 Bad Gateway</html>");
assert_eq!(err.detail(), Some("<html>502 Bad Gateway</html>"));
}
#[test]
fn truncates_on_char_boundaries() {
assert_eq!(truncate("æææ", 3), "æ…");
}
}