use cratestack_core::{CoolError, CoolErrorResponse};
use reqwest::StatusCode;
pub type HeaderPair<'a> = (&'a str, &'a str);
pub type QueryPair<'a> = (&'a str, &'a str);
#[derive(Debug)]
pub struct TransportError {
inner: Box<reqwest::Error>,
}
impl TransportError {
pub fn reqwest_error(&self) -> &reqwest::Error {
&self.inner
}
pub fn into_source(self) -> reqwest::Error {
*self.inner
}
}
impl std::fmt::Display for TransportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.inner)
}
}
impl std::error::Error for TransportError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.inner.as_ref())
}
}
impl From<reqwest::Error> for TransportError {
fn from(e: reqwest::Error) -> Self {
TransportError { inner: Box::new(e) }
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClientError {
#[error("transport error: {0}")]
Transport(#[source] TransportError),
#[error("codec error: {0}")]
Codec(#[from] CoolError),
#[error("state error: {0}")]
State(String),
#[error("invalid response: {0}")]
InvalidResponse(String),
#[error("bad input: {0}")]
BadInput(String),
#[error("remote call failed with status {status}: {message}")]
Remote {
status: StatusCode,
error: Option<CoolErrorResponse>,
message: String,
},
}
impl From<reqwest::Error> for ClientError {
fn from(e: reqwest::Error) -> Self {
ClientError::Transport(TransportError::from(e))
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn synthesize_reqwest_error() -> reqwest::Error {
let _ = rustls::crypto::ring::default_provider().install_default();
reqwest::Client::new()
.get("http://127.0.0.1:1")
.send()
.await
.expect_err("connecting to a closed loopback port should fail")
}
#[tokio::test]
async fn transport_error_display_and_accessor_forward_to_reqwest() {
let reqwest_err = synthesize_reqwest_error().await;
let expected_message = reqwest_err.to_string();
let transport = TransportError::from(reqwest_err);
assert_eq!(transport.to_string(), expected_message);
assert_eq!(transport.reqwest_error().to_string(), expected_message);
}
#[tokio::test]
async fn transport_error_into_source_round_trips_the_reqwest_error() {
let reqwest_err = synthesize_reqwest_error().await;
let expected_message = reqwest_err.to_string();
let recovered = TransportError::from(reqwest_err).into_source();
assert_eq!(recovered.to_string(), expected_message);
}
#[tokio::test]
async fn client_error_transport_chains_via_std_error_source() {
let reqwest_err = synthesize_reqwest_error().await;
let expected_message = reqwest_err.to_string();
let client_err = ClientError::from(reqwest_err);
assert!(matches!(client_err, ClientError::Transport(_)));
assert!(client_err.to_string().contains(&expected_message));
let source = std::error::Error::source(&client_err)
.expect("ClientError::Transport should chain to the reqwest::Error via source()");
assert_eq!(source.to_string(), expected_message);
}
#[test]
fn client_error_variants() {
let transport_err = ClientError::State("state error".to_string());
assert!(matches!(transport_err, ClientError::State(_)));
let invalid_resp = ClientError::InvalidResponse("invalid".to_string());
assert!(matches!(invalid_resp, ClientError::InvalidResponse(_)));
let bad_input = ClientError::BadInput("bad".to_string());
assert!(matches!(bad_input, ClientError::BadInput(_)));
}
#[test]
fn client_error_displays_correctly() {
let err = ClientError::State("test error".to_string());
assert_eq!(err.to_string(), "state error: test error");
let err = ClientError::InvalidResponse("invalid".to_string());
assert_eq!(err.to_string(), "invalid response: invalid");
}
}