use std::{
borrow::Cow,
error::Error as StdError,
fmt,
time::{Duration, SystemTime},
};
use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode, Uri, header};
use serde::{Deserialize, de::IgnoredAny};
use crate::{
codec::{self, DecodeError, DecodeErrorKind, RawJson},
constants::{RETRY_AFTER_MS_HEADER, request_id},
text,
};
type Cause = Box<dyn StdError + Send + Sync>;
pub struct Error(Box<Inner>);
struct Inner {
kind: ErrorKind,
message: Box<str>,
source: Option<Cause>,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ErrorKind {
Config,
InvalidRequest,
Api(ApiError),
Connection,
Timeout {
timeout: Duration,
},
ResponseValidation(ResponseValidationError),
ResponseTooLarge {
limit: usize,
},
}
impl Error {
pub fn kind(&self) -> &ErrorKind {
&self.0.kind
}
pub(crate) fn config(message: impl Into<Box<str>>) -> Self {
Self::plain(ErrorKind::Config, message, None)
}
pub(crate) fn invalid_request(message: impl Into<Box<str>>) -> Self {
Self::plain(ErrorKind::InvalidRequest, message, None)
}
pub(crate) fn connection(message: impl Into<Box<str>>, cause: Option<Cause>) -> Self {
Self::plain(ErrorKind::Connection, message, cause)
}
pub(crate) fn into_parts(self) -> (Box<str>, Option<Cause>) {
let Inner { message, source, .. } = *self.0;
(message, source)
}
pub(crate) fn timeout(timeout: Duration) -> Self {
Self::plain(ErrorKind::Timeout { timeout }, "", None)
}
pub(crate) fn response_too_large(limit: usize) -> Self {
Self::plain(ErrorKind::ResponseTooLarge { limit }, "", None)
}
fn plain(kind: ErrorKind, message: impl Into<Box<str>>, source: Option<Cause>) -> Self {
Self(Box::new(Inner { kind, message: message.into(), source }))
}
}
impl From<ApiError> for Error {
fn from(error: ApiError) -> Self {
Self::plain(ErrorKind::Api(error), "", None)
}
}
impl From<ResponseValidationError> for Error {
fn from(error: ResponseValidationError) -> Self {
Self::plain(ErrorKind::ResponseValidation(error), "", None)
}
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0.kind {
ErrorKind::Api(error) => error.fmt(formatter),
ErrorKind::ResponseValidation(error) => error.fmt(formatter),
ErrorKind::Timeout { timeout } => {
write!(formatter, "Request timed out (timeout={}s).", timeout.as_secs_f64())
}
ErrorKind::ResponseTooLarge { limit } => write!(
formatter,
"The response body exceeded the limit of {limit} bytes and was not read."
),
ErrorKind::Config | ErrorKind::InvalidRequest | ErrorKind::Connection => {
formatter.write_str(&self.0.message)
}
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut shown = formatter.debug_struct("Error");
shown.field("kind", &self.0.kind);
if !self.0.message.is_empty() {
shown.field("message", &self.0.message);
}
if let Some(source) = &self.0.source {
shown.field("source", source);
}
shown.finish()
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match &self.0.kind {
ErrorKind::Api(_) => None,
ErrorKind::ResponseValidation(error) => Some(error.decode_error()),
_ => self.0.source.as_ref().map(|cause| &**cause as &(dyn StdError + 'static)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ApiErrorKind {
BadRequest,
Authentication,
PermissionDenied,
NotFound,
UnprocessableEntity,
RateLimit,
InternalServer,
Other,
}
impl ApiErrorKind {
fn of(status: StatusCode) -> Self {
match status.as_u16() {
400 => Self::BadRequest,
401 => Self::Authentication,
403 => Self::PermissionDenied,
404 => Self::NotFound,
422 => Self::UnprocessableEntity,
429 => Self::RateLimit,
500.. => Self::InternalServer,
_ => Self::Other,
}
}
}
#[derive(Clone)]
pub struct ApiError {
status: StatusCode,
headers: HeaderMap,
body: Bytes,
endpoint: Option<Box<str>>,
message: Box<str>,
error_type: Option<Box<str>>,
}
impl ApiError {
pub(crate) fn new(
status: StatusCode,
body: Bytes,
headers: HeaderMap,
endpoint: Option<Box<str>>,
) -> Self {
let reading = BodyReading::of(&body);
Self {
status,
headers,
body,
endpoint,
message: reading.message,
error_type: reading.error_type,
}
}
pub(crate) fn with_message(
status: StatusCode,
body: Bytes,
headers: HeaderMap,
endpoint: Option<Box<str>>,
message: impl Into<Box<str>>,
) -> Self {
let reading = BodyReading::of(&body);
Self {
status,
headers,
body,
endpoint,
message: message.into(),
error_type: reading.error_type,
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn kind(&self) -> ApiErrorKind {
ApiErrorKind::of(self.status)
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn request_id(&self) -> Option<&str> {
request_id(&self.headers)
}
pub fn endpoint(&self) -> Option<&str> {
self.endpoint.as_deref()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn error_type(&self) -> Option<&str> {
self.error_type.as_deref()
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn body_text(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.body)
}
pub fn body_json<'de, T>(&'de self) -> Result<T, DecodeError>
where
T: Deserialize<'de>,
{
codec::decode(&self.body)
}
pub fn retry_after(&self) -> Option<Duration> {
parse_retry_after(&self.headers, SystemTime::now())
}
}
impl fmt::Display for ApiError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
render(formatter, self.endpoint(), self.status, &self.message, self.request_id())
}
}
impl fmt::Debug for ApiError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ApiError")
.field("status", &self.status.as_u16())
.field("kind", &self.kind())
.field("endpoint", &self.endpoint())
.field("request_id", &self.request_id().map(shown_name))
.field("message", &self.message)
.field("error_type", &self.error_type().map(shown_name))
.field("headers", &HeaderCount(self.headers.len()))
.field("body", &ByteCount(self.body.len()))
.finish()
}
}
impl StdError for ApiError {}
#[derive(Clone)]
pub struct ResponseValidationError {
status: StatusCode,
headers: HeaderMap,
body: Bytes,
endpoint: Option<Box<str>>,
source: DecodeError,
message: Box<str>,
}
impl ResponseValidationError {
pub(crate) fn new(
status: StatusCode,
body: Bytes,
headers: HeaderMap,
endpoint: Option<Box<str>>,
source: DecodeError,
) -> Self {
let message = format!("Invalid response data at '{}'.", source.path()).into_boxed_str();
Self { status, headers, body, endpoint, source, message }
}
pub fn field_path(&self) -> &str {
self.source.path()
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn headers(&self) -> &HeaderMap {
&self.headers
}
pub fn request_id(&self) -> Option<&str> {
request_id(&self.headers)
}
pub fn endpoint(&self) -> Option<&str> {
self.endpoint.as_deref()
}
pub fn message(&self) -> &str {
&self.message
}
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn body_text(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.body)
}
pub fn body_json<'de, T>(&'de self) -> Result<T, DecodeError>
where
T: Deserialize<'de>,
{
codec::decode(&self.body)
}
pub fn decode_error(&self) -> &DecodeError {
&self.source
}
}
impl fmt::Display for ResponseValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
render(formatter, self.endpoint(), self.status, &self.message, self.request_id())
}
}
impl fmt::Debug for ResponseValidationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ResponseValidationError")
.field("status", &self.status.as_u16())
.field("endpoint", &self.endpoint())
.field("request_id", &self.request_id().map(shown_name))
.field("field_path", &self.field_path())
.field("source", &self.source)
.field("headers", &HeaderCount(self.headers.len()))
.field("body", &ByteCount(self.body.len()))
.finish()
}
}
impl StdError for ResponseValidationError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&self.source)
}
}
fn render(
formatter: &mut fmt::Formatter<'_>,
endpoint: Option<&str>,
status: StatusCode,
message: &str,
request_id: Option<&str>,
) -> fmt::Result {
if let Some(endpoint) = endpoint {
write!(formatter, "{endpoint}: ")?;
}
write!(formatter, "{}", status.as_u16())?;
if !message.is_empty() {
write!(formatter, " {message}")?;
}
if let Some(request_id) = request_id {
write!(formatter, " (request_id={})", shown_name(request_id))?;
}
Ok(())
}
fn shown_name(name: &str) -> String {
text::bounded(&name, text::MAX_NAME_CHARS)
}
struct HeaderCount(usize);
impl fmt::Debug for HeaderCount {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "<{} redacted>", self.0)
}
}
struct ByteCount(usize);
impl fmt::Debug for ByteCount {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "<{} bytes>", self.0)
}
}
pub(crate) fn format_endpoint(method: &Method, uri: &Uri) -> String {
let mut out = String::with_capacity(method.as_str().len() + 1 + uri.path().len() + 32);
out.push_str(method.as_str());
out.push(' ');
if let Some(scheme) = uri.scheme() {
out.push_str(scheme.as_str());
out.push_str("://");
}
if let Some(host) = uri.host() {
out.push_str(host);
if let Some(port) = uri.port_u16()
&& default_port(uri.scheme_str()) != Some(port)
{
out.push(':');
out.push_str(&port.to_string());
}
}
out.push_str(uri.path());
out
}
fn default_port(scheme: Option<&str>) -> Option<u16> {
match scheme {
Some("http") => Some(80),
Some("https") => Some(443),
_ => None,
}
}
pub(crate) fn parse_retry_after(headers: &HeaderMap, now: SystemTime) -> Option<Duration> {
for (name, per_unit) in
[(RETRY_AFTER_MS_HEADER, 1.0_f64), (header::RETRY_AFTER.as_str(), 1000.0_f64)]
{
let Some(raw) = headers.get(name).and_then(|value| value.to_str().ok()) else {
continue;
};
let trimmed = raw.trim();
let spelled = if trimmed.is_empty() { "0" } else { trimmed };
match spelled.parse::<f64>() {
Ok(seconds) if !seconds.is_finite() => {}
Ok(seconds) if seconds >= 0.0 => {
let millis = seconds * per_unit;
if millis.is_finite() {
return Some(millis_to_duration(millis));
}
}
Ok(_) if name == header::RETRY_AFTER.as_str() => return None,
Ok(_) => {}
Err(_) if name == header::RETRY_AFTER.as_str() => {
if let Ok(when) = httpdate::parse_http_date(raw) {
let wait = when.duration_since(now).unwrap_or(Duration::ZERO);
return Some(Duration::from_millis(
u64::try_from(wait.as_millis()).unwrap_or(u64::MAX),
));
}
}
Err(_) => {}
}
}
None
}
fn millis_to_duration(millis: f64) -> Duration {
Duration::from_millis(millis as u64)
}
struct BodyReading {
message: Box<str>,
error_type: Option<Box<str>>,
}
#[derive(Deserialize)]
struct Envelope {
error: Option<RawJson>,
message: Option<RawJson>,
detail: Option<RawJson>,
}
#[derive(Deserialize)]
struct Detail {
message: Option<RawJson>,
error_type: Option<RawJson>,
}
#[derive(Deserialize)]
struct MessageMember {
message: Option<RawJson>,
}
#[derive(Deserialize)]
struct DetailEntry {
msg: Option<RawJson>,
loc: Option<RawJson>,
}
impl BodyReading {
fn of(body: &[u8]) -> Self {
if body.is_empty() {
return Self::no_body();
}
match first_token(body) {
Some(b'{') => Self::of_object(body),
Some(b'"') => match codec::decode::<String>(body) {
Ok(text) => Self::said(bounded(&text)),
Err(_) => Self::said(text_message(body)),
},
_ => match codec::decode::<Option<IgnoredAny>>(body) {
Ok(None) => Self::no_body(),
Ok(Some(_)) => Self::said(json_message(body)),
Err(failure) => Self::said(unparsed_message(body, &failure)),
},
}
}
fn no_body() -> Self {
Self { message: "status code (no body)".into(), error_type: None }
}
fn said(message: impl Into<Box<str>>) -> Self {
Self { message: message.into(), error_type: None }
}
fn of_object(body: &[u8]) -> Self {
let envelope = match codec::decode::<Envelope>(body) {
Ok(envelope) => envelope,
Err(failure) => return Self::said(unparsed_message(body, &failure)),
};
let detail = envelope.detail.as_ref().and_then(|raw| raw.decode::<Detail>().ok());
let error_type = detail
.as_ref()
.and_then(|detail| detail.error_type.as_ref())
.and_then(as_text)
.map(Into::into);
let message = as_text_of(&envelope.error)
.or_else(|| {
member_text(&envelope.error, |raw| {
raw.decode::<MessageMember>().ok().and_then(|it| it.message)
})
})
.or_else(|| as_text_of(&envelope.message))
.or_else(|| as_text_of(&envelope.detail))
.or_else(|| {
detail.as_ref().and_then(|detail| detail.message.as_ref()).and_then(as_text)
})
.or_else(|| joined_detail_list(&envelope.detail))
.filter(|message| !message.is_empty());
Self {
message: message.map_or_else(|| json_message(body), |message| bounded(&message)),
error_type,
}
}
}
fn as_text(raw: &RawJson) -> Option<String> {
raw.decode::<String>().ok()
}
fn as_text_of(raw: &Option<RawJson>) -> Option<String> {
raw.as_ref().and_then(as_text)
}
fn member_text(
raw: &Option<RawJson>,
member: impl FnOnce(&RawJson) -> Option<RawJson>,
) -> Option<String> {
raw.as_ref().and_then(member).as_ref().and_then(as_text)
}
fn joined_detail_list(raw: &Option<RawJson>) -> Option<String> {
let entries = raw.as_ref()?.decode::<Vec<RawJson>>().ok()?;
let mut parts = Vec::with_capacity(entries.len());
for entry in &entries {
let Ok(entry) = entry.decode::<DetailEntry>() else {
continue;
};
let Some(message) = entry.msg.as_ref().and_then(as_text) else {
continue;
};
let path = entry.loc.as_ref().map(location_path).unwrap_or_default();
parts.push(if path.is_empty() { message } else { format!("{path}: {message}") });
}
if parts.is_empty() { None } else { Some(parts.join("; ")) }
}
fn location_path(raw: &RawJson) -> String {
let Ok(segments) = raw.decode::<Vec<RawJson>>() else {
return String::new();
};
let mut path = String::new();
for segment in &segments {
let rendered = match segment.decode::<String>() {
Ok(name) if name == "body" => continue,
Ok(name) => name,
Err(_) => match segment.decode::<i64>() {
Ok(index) => index.to_string(),
Err(_) => continue,
},
};
if !path.is_empty() {
path.push('.');
}
path.push_str(&rendered);
}
path
}
fn json_message(body: &[u8]) -> Box<str> {
let text = String::from_utf8_lossy(body);
bounded(&compact(&text))
}
fn text_message(body: &[u8]) -> Box<str> {
bounded(&String::from_utf8_lossy(body))
}
fn unparsed_message(body: &[u8], failure: &DecodeError) -> Box<str> {
if failure.kind() == DecodeErrorKind::TooDeep { json_message(body) } else { text_message(body) }
}
fn bounded(text: &str) -> Box<str> {
text::bounded(&text, text::MAX_MESSAGE_CHARS).into_boxed_str()
}
fn first_token(body: &[u8]) -> Option<u8> {
body.iter().copied().find(|byte| !byte.is_ascii_whitespace())
}
fn compact(text: &str) -> Cow<'_, str> {
let mut in_string = false;
let mut escaped = false;
let mut kept: Option<String> = None;
let mut copied = 0;
for (at, byte) in text.bytes().enumerate() {
if in_string {
match byte {
_ if escaped => escaped = false,
b'\\' => escaped = true,
b'"' => in_string = false,
_ => {}
}
continue;
}
match byte {
b'"' => in_string = true,
b' ' | b'\t' | b'\n' | b'\r' => {
let kept = kept.get_or_insert_with(|| String::with_capacity(text.len()));
kept.push_str(&text[copied..at]);
copied = at + 1;
}
_ => {}
}
}
match kept {
Some(mut kept) => {
kept.push_str(&text[copied..]);
Cow::Owned(kept)
}
None => Cow::Borrowed(text),
}
}
#[cfg(test)]
#[path = "error_tests.rs"]
mod tests;