#![doc = include_str!("../README.md")]
#![doc(test(attr(deny(warnings))))]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TwirpError {
code: TwirpErrorCode,
msg: String,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "HashMap::is_empty")
)]
meta: HashMap<String, String>,
#[cfg_attr(feature = "serde", serde(default, skip))]
source: Option<Arc<dyn Error + Send + Sync>>,
}
impl TwirpError {
#[inline]
pub fn code(&self) -> TwirpErrorCode {
self.code
}
#[inline]
pub fn message(&self) -> &str {
&self.msg
}
#[inline]
pub fn into_message(self) -> String {
self.msg
}
#[inline]
pub fn meta(&self, key: &str) -> Option<&str> {
self.meta.get(key).map(|s| s.as_str())
}
#[inline]
pub fn meta_iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.meta.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
#[inline]
pub fn new(code: TwirpErrorCode, msg: impl Into<String>) -> Self {
Self {
code,
msg: msg.into(),
meta: HashMap::new(),
source: None,
}
}
#[inline]
pub fn wrap(
code: TwirpErrorCode,
msg: impl Into<String>,
e: impl Error + Send + Sync + 'static,
) -> Self {
Self {
code,
msg: msg.into(),
meta: HashMap::new(),
source: Some(Arc::new(e)),
}
}
#[inline]
pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.meta.insert(key.into(), value.into());
self
}
#[inline]
pub fn aborted(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Aborted, msg)
}
#[inline]
pub fn already_exists(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::AlreadyExists, msg)
}
#[inline]
pub fn canceled(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Canceled, msg)
}
#[inline]
pub fn dataloss(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Dataloss, msg)
}
#[inline]
pub fn invalid_argument(argument: impl Into<String>, msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::InvalidArgument, msg).with_meta("argument", argument)
}
#[inline]
pub fn internal(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Internal, msg)
}
#[inline]
pub fn deadline_exceeded(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::DeadlineExceeded, msg)
}
#[inline]
pub fn failed_precondition(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::FailedPrecondition, msg)
}
#[inline]
pub fn malformed(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Malformed, msg)
}
#[inline]
pub fn not_found(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::NotFound, msg)
}
#[inline]
pub fn out_of_range(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::OutOfRange, msg)
}
#[inline]
pub fn permission_denied(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::PermissionDenied, msg)
}
#[inline]
pub fn required_argument(argument: &str) -> Self {
Self::invalid_argument(argument, format!("{argument} is required"))
}
#[inline]
pub fn resource_exhausted(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::ResourceExhausted, msg)
}
#[inline]
pub fn unauthenticated(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Unauthenticated, msg)
}
#[inline]
pub fn unavailable(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Unavailable, msg)
}
#[inline]
pub fn unimplemented(msg: impl Into<String>) -> Self {
Self::new(TwirpErrorCode::Unimplemented, msg)
}
}
impl fmt::Display for TwirpError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Twirp {:?} error: {}", self.code, self.msg)
}
}
impl Error for TwirpError {
#[inline]
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.source.as_ref()?)
}
}
impl PartialEq for TwirpError {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.code == other.code && self.msg == other.msg && self.meta == other.meta
}
}
impl Eq for TwirpError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum TwirpErrorCode {
Canceled,
Unknown,
InvalidArgument,
Malformed,
DeadlineExceeded,
NotFound,
BadRoute,
AlreadyExists,
PermissionDenied,
Unauthenticated,
ResourceExhausted,
FailedPrecondition,
Aborted,
OutOfRange,
Unimplemented,
Internal,
Unavailable,
Dataloss,
}
#[cfg(feature = "http")]
impl From<TwirpErrorCode> for http::StatusCode {
#[inline]
fn from(code: TwirpErrorCode) -> Self {
match code {
TwirpErrorCode::Canceled => Self::REQUEST_TIMEOUT,
TwirpErrorCode::Unknown => Self::INTERNAL_SERVER_ERROR,
TwirpErrorCode::InvalidArgument => Self::BAD_REQUEST,
TwirpErrorCode::Malformed => Self::BAD_REQUEST,
TwirpErrorCode::DeadlineExceeded => Self::REQUEST_TIMEOUT,
TwirpErrorCode::NotFound => Self::NOT_FOUND,
TwirpErrorCode::BadRoute => Self::NOT_FOUND,
TwirpErrorCode::AlreadyExists => Self::CONFLICT,
TwirpErrorCode::PermissionDenied => Self::FORBIDDEN,
TwirpErrorCode::Unauthenticated => Self::UNAUTHORIZED,
TwirpErrorCode::ResourceExhausted => Self::TOO_MANY_REQUESTS,
TwirpErrorCode::FailedPrecondition => Self::PRECONDITION_FAILED,
TwirpErrorCode::Aborted => Self::CONFLICT,
TwirpErrorCode::OutOfRange => Self::BAD_REQUEST,
TwirpErrorCode::Unimplemented => Self::NOT_IMPLEMENTED,
TwirpErrorCode::Internal => Self::INTERNAL_SERVER_ERROR,
TwirpErrorCode::Unavailable => Self::SERVICE_UNAVAILABLE,
TwirpErrorCode::Dataloss => Self::SERVICE_UNAVAILABLE,
}
}
}
#[cfg(feature = "http")]
impl<B: From<String>> From<TwirpError> for http::Response<B> {
fn from(error: TwirpError) -> Self {
let json = serde_json::to_string(&error).unwrap();
http::Response::builder()
.status(error.code)
.header(http::header::CONTENT_TYPE, "application/json")
.extension(error)
.body(json.into())
.unwrap()
}
}
#[cfg(feature = "http")]
impl<B: AsRef<[u8]>> From<http::Response<B>> for TwirpError {
fn from(response: http::Response<B>) -> Self {
if let Some(error) = response.extensions().get::<Self>() {
return error.clone();
}
let status = response.status();
let body = response.into_body();
if let Ok(error) = serde_json::from_slice::<TwirpError>(body.as_ref()) {
return error;
}
let code = if status == http::StatusCode::REQUEST_TIMEOUT {
TwirpErrorCode::DeadlineExceeded
} else if status == http::StatusCode::FORBIDDEN {
TwirpErrorCode::PermissionDenied
} else if status == http::StatusCode::UNAUTHORIZED {
TwirpErrorCode::Unauthenticated
} else if status == http::StatusCode::TOO_MANY_REQUESTS {
TwirpErrorCode::ResourceExhausted
} else if status == http::StatusCode::PRECONDITION_FAILED {
TwirpErrorCode::FailedPrecondition
} else if status == http::StatusCode::NOT_IMPLEMENTED {
TwirpErrorCode::Unimplemented
} else if status == http::StatusCode::TOO_MANY_REQUESTS
|| status == http::StatusCode::BAD_GATEWAY
|| status == http::StatusCode::SERVICE_UNAVAILABLE
|| status == http::StatusCode::GATEWAY_TIMEOUT
{
TwirpErrorCode::Unavailable
} else if status == http::StatusCode::NOT_FOUND {
TwirpErrorCode::NotFound
} else if status.is_server_error() {
TwirpErrorCode::Internal
} else if status.is_client_error() {
TwirpErrorCode::Malformed
} else {
TwirpErrorCode::Unknown
};
TwirpError::new(code, String::from_utf8_lossy(body.as_ref()))
}
}
#[cfg(feature = "axum-07")]
impl axum_core_04::response::IntoResponse for TwirpError {
#[inline]
fn into_response(self) -> axum_core_04::response::Response {
self.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "http")]
use std::error::Error;
#[test]
fn test_accessors() {
let error = TwirpError::invalid_argument("foo", "foo is wrong");
assert_eq!(error.code(), TwirpErrorCode::InvalidArgument);
assert_eq!(error.message(), "foo is wrong");
assert_eq!(error.meta("argument"), Some("foo"));
}
#[cfg(feature = "http")]
#[test]
fn test_to_response() -> Result<(), Box<dyn Error>> {
let object =
TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog");
let response = http::Response::<Vec<u8>>::from(object);
assert_eq!(response.status(), http::StatusCode::FORBIDDEN);
assert_eq!(
response.headers().get(http::header::CONTENT_TYPE),
Some(&http::HeaderValue::from_static("application/json"))
);
assert_eq!(
response.into_body(), b"{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}"
);
Ok(())
}
#[cfg(feature = "http")]
#[test]
fn test_from_valid_response() -> Result<(), Box<dyn Error>> {
let response = http::Response::builder()
.header(http::header::CONTENT_TYPE, "application/json")
.body("{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}")?;
assert_eq!(
TwirpError::from(response),
TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog")
);
Ok(())
}
#[cfg(feature = "http")]
#[test]
fn test_from_plain_response() -> Result<(), Box<dyn Error>> {
let response = http::Response::builder()
.status(http::StatusCode::FORBIDDEN)
.body("Thou shall not pass")?;
assert_eq!(
TwirpError::from(response),
TwirpError::permission_denied("Thou shall not pass")
);
Ok(())
}
}