use crate::xrpc::EncodeError;
use alloc::boxed::Box;
use alloc::string::ToString;
use bytes::Bytes;
use smol_str::SmolStr;
#[cfg(feature = "std")]
use miette::Diagnostic;
pub type BoxError = Box<dyn core::error::Error + Send + Sync + 'static>;
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
#[error("{kind}")]
pub struct ClientError {
#[cfg_attr(feature = "std", diagnostic_source)]
kind: ClientErrorKind,
#[source]
source: Option<BoxError>,
#[cfg_attr(feature = "std", help)]
help: Option<SmolStr>,
context: Option<SmolStr>,
url: Option<SmolStr>,
details: Option<SmolStr>,
location: Option<SmolStr>,
}
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
#[non_exhaustive]
pub enum ClientErrorKind {
#[error("transport error")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::client::transport)))]
Transport,
#[error("invalid request: {0}")]
#[cfg_attr(
feature = "std",
diagnostic(
code(jacquard::client::invalid_request),
help("check request parameters and format")
)
)]
InvalidRequest(SmolStr),
#[error("encode error: {0}")]
#[cfg_attr(
feature = "std",
diagnostic(
code(jacquard::client::encode),
help("check request body format and encoding")
)
)]
Encode(SmolStr),
#[error("decode error: {0}")]
#[cfg_attr(
feature = "std",
diagnostic(
code(jacquard::client::decode),
help("check response format and encoding")
)
)]
Decode(SmolStr),
#[error("HTTP {status}")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::client::http)))]
Http {
status: http::StatusCode,
},
#[error("auth error: {0}")]
#[cfg_attr(feature = "std", diagnostic(code(jacquard::client::auth)))]
Auth(AuthError),
#[error("identity resolution failed")]
#[cfg_attr(
feature = "std",
diagnostic(
code(jacquard::client::identity_resolution),
help("check handle/DID is valid and network is accessible")
)
)]
IdentityResolution,
#[error("storage error")]
#[cfg_attr(
feature = "std",
diagnostic(
code(jacquard::client::storage),
help("check storage backend is accessible and has sufficient permissions")
)
)]
Storage,
}
impl ClientError {
pub fn new(kind: ClientErrorKind, source: Option<BoxError>) -> Self {
Self {
kind,
source,
help: None,
context: None,
url: None,
details: None,
location: None,
}
}
pub fn kind(&self) -> &ClientErrorKind {
&self.kind
}
pub fn source_err(&self) -> Option<&BoxError> {
self.source.as_ref()
}
pub fn context(&self) -> Option<&str> {
self.context.as_ref().map(|s| s.as_str())
}
pub fn url(&self) -> Option<&str> {
self.url.as_ref().map(|s| s.as_str())
}
pub fn details(&self) -> Option<&str> {
self.details.as_ref().map(|s| s.as_str())
}
pub fn location(&self) -> Option<&str> {
self.location.as_ref().map(|s| s.as_str())
}
pub fn with_help(mut self, help: impl Into<SmolStr>) -> Self {
self.help = Some(help.into());
self
}
pub fn with_context(mut self, context: impl Into<SmolStr>) -> Self {
self.context = Some(context.into());
self
}
pub fn with_url(mut self, url: impl Into<SmolStr>) -> Self {
self.url = Some(url.into());
self
}
pub fn with_details(mut self, details: impl Into<SmolStr>) -> Self {
self.details = Some(details.into());
self
}
pub fn with_location(mut self, location: impl Into<SmolStr>) -> Self {
self.location = Some(location.into());
self
}
pub fn append_context(mut self, additional: impl AsRef<str>) -> Self {
self.context = Some(match self.context.take() {
Some(existing) => smol_str::format_smolstr!("{}: {}", existing, additional.as_ref()),
None => additional.as_ref().into(),
});
self
}
pub fn for_nsid(self, nsid: &str) -> Self {
self.append_context(smol_str::format_smolstr!("[{}]", nsid))
}
pub fn for_collection(self, operation: &str, collection_nsid: &str) -> Self {
self.append_context(smol_str::format_smolstr!(
"{} [{}]",
operation,
collection_nsid
))
}
pub fn transport(source: impl core::error::Error + Send + Sync + 'static) -> Self {
Self::new(ClientErrorKind::Transport, Some(Box::new(source)))
}
pub fn invalid_request(msg: impl Into<SmolStr>) -> Self {
Self::new(ClientErrorKind::InvalidRequest(msg.into()), None)
}
pub fn encode(msg: impl Into<SmolStr>) -> Self {
Self::new(ClientErrorKind::Encode(msg.into()), None)
}
pub fn decode(msg: impl Into<SmolStr>) -> Self {
Self::new(ClientErrorKind::Decode(msg.into()), None)
}
pub fn http(status: http::StatusCode, body: Option<Bytes>) -> Self {
let http_err = HttpError { status, body };
Self::new(ClientErrorKind::Http { status }, Some(Box::new(http_err)))
}
pub fn auth(auth_error: AuthError) -> Self {
Self::new(ClientErrorKind::Auth(auth_error), None)
}
pub fn identity_resolution(source: impl core::error::Error + Send + Sync + 'static) -> Self {
Self::new(ClientErrorKind::IdentityResolution, Some(Box::new(source)))
}
pub fn storage(source: impl core::error::Error + Send + Sync + 'static) -> Self {
Self::new(ClientErrorKind::Storage, Some(Box::new(source)))
}
}
pub type XrpcResult<T> = Result<T, ClientError>;
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
#[non_exhaustive]
pub enum DecodeError {
#[error("Failed to deserialize JSON: {0}")]
Json(
#[from]
#[source]
serde_json::Error,
),
#[cfg(feature = "std")]
#[error("Failed to deserialize CBOR: {0}")]
CborLocal(
#[from]
#[source]
serde_ipld_dagcbor::DecodeError<std::io::Error>,
),
#[error("Failed to deserialize CBOR: {0}")]
CborRemote(
#[from]
#[source]
serde_ipld_dagcbor::DecodeError<HttpError>,
),
#[error("Failed to deserialize DAG-CBOR: {0}")]
DagCborInfallible(
#[from]
#[source]
serde_ipld_dagcbor::DecodeError<core::convert::Infallible>,
),
#[cfg(all(feature = "websocket", feature = "std"))]
#[error("Failed to deserialize cbor header: {0}")]
CborHeader(
#[from]
#[source]
ciborium::de::Error<std::io::Error>,
),
#[cfg(all(feature = "websocket", not(feature = "std")))]
#[error("Failed to deserialize cbor header: {0}")]
CborHeader(
#[from]
#[source]
ciborium::de::Error<core::convert::Infallible>,
),
#[cfg(feature = "websocket")]
#[error("Unknown event type: {0}")]
UnknownEventType(smol_str::SmolStr),
}
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
pub struct HttpError {
pub status: http::StatusCode,
pub body: Option<Bytes>,
}
impl core::fmt::Display for HttpError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "HTTP {}", self.status)?;
if let Some(body) = &self.body {
if let Ok(s) = core::str::from_utf8(body) {
write!(f, ":\n{}", s)?;
}
}
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "std", derive(Diagnostic))]
#[non_exhaustive]
pub enum AuthError {
#[error("Access token expired")]
TokenExpired,
#[error("Invalid access token")]
InvalidToken,
#[error("Token refresh failed")]
RefreshFailed,
#[error("No authentication provided, but endpoint requires auth")]
NotAuthenticated,
#[error("DPoP proof construction failed")]
DpopProofFailed,
#[error("DPoP nonce negotiation failed")]
DpopNonceFailed,
#[error("Authentication error: {0:?}")]
Other(http::HeaderValue),
}
impl crate::IntoStatic for AuthError {
type Output = AuthError;
fn into_static(self) -> Self::Output {
match self {
AuthError::TokenExpired => AuthError::TokenExpired,
AuthError::InvalidToken => AuthError::InvalidToken,
AuthError::RefreshFailed => AuthError::RefreshFailed,
AuthError::NotAuthenticated => AuthError::NotAuthenticated,
AuthError::DpopProofFailed => AuthError::DpopProofFailed,
AuthError::DpopNonceFailed => AuthError::DpopNonceFailed,
AuthError::Other(header) => AuthError::Other(header),
}
}
}
impl From<DecodeError> for ClientError {
fn from(e: DecodeError) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
.with_context("response deserialization failed")
}
}
impl From<HttpError> for ClientError {
fn from(e: HttpError) -> Self {
Self::http(e.status, e.body)
}
}
impl From<AuthError> for ClientError {
fn from(e: AuthError) -> Self {
Self::auth(e)
}
}
impl From<EncodeError> for ClientError {
fn from(e: EncodeError) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Encode(msg), Some(Box::new(e)))
.with_context("request encoding failed")
}
}
#[cfg(feature = "reqwest-client")]
impl From<reqwest::Error> for ClientError {
#[cfg(not(target_arch = "wasm32"))]
fn from(e: reqwest::Error) -> Self {
Self::transport(e)
}
#[cfg(target_arch = "wasm32")]
fn from(e: reqwest::Error) -> Self {
Self::transport(e)
}
}
impl From<serde_json::Error> for ClientError {
fn from(e: serde_json::Error) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
.with_context("JSON deserialization failed")
}
}
#[cfg(feature = "std")]
impl From<serde_ipld_dagcbor::DecodeError<std::io::Error>> for ClientError {
fn from(e: serde_ipld_dagcbor::DecodeError<std::io::Error>) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
.with_context("DAG-CBOR deserialization failed (local I/O)")
}
}
impl From<serde_ipld_dagcbor::DecodeError<HttpError>> for ClientError {
fn from(e: serde_ipld_dagcbor::DecodeError<HttpError>) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
.with_context("DAG-CBOR deserialization failed (remote)")
}
}
impl From<serde_ipld_dagcbor::DecodeError<core::convert::Infallible>> for ClientError {
fn from(e: serde_ipld_dagcbor::DecodeError<core::convert::Infallible>) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
.with_context("DAG-CBOR deserialization failed (in-memory)")
}
}
#[cfg(all(feature = "websocket", feature = "std"))]
impl From<ciborium::de::Error<std::io::Error>> for ClientError {
fn from(e: ciborium::de::Error<std::io::Error>) -> Self {
let msg = smol_str::format_smolstr!("{:?}", e);
Self::new(ClientErrorKind::Decode(msg), Some(Box::new(e)))
.with_context("CBOR header deserialization failed")
}
}
impl From<crate::session::SessionStoreError> for ClientError {
fn from(e: crate::session::SessionStoreError) -> Self {
Self::storage(e)
}
}
impl From<crate::deps::fluent_uri::ParseError> for ClientError {
fn from(e: crate::deps::fluent_uri::ParseError) -> Self {
Self::invalid_request(e.to_string())
}
}