use thiserror::Error;
#[derive(Debug, Error)]
pub enum BuildError {
#[cfg(not(target_arch = "wasm32"))]
#[error("Root certificate bundle contained no certificates")]
EmptyRootCertificates,
#[cfg(not(target_arch = "wasm32"))]
#[error("Failed to parse root certificate PEM: {0}")]
RootCertificatePem(#[from] rustls::pki_types::pem::Error),
#[cfg(not(target_arch = "wasm32"))]
#[error("Invalid root certificate: {0}")]
RootCertificate(#[from] rustls::Error),
#[error("Failed to build the Pkarr client: {0}")]
Pkarr(#[from] pkarr::errors::BuildError),
#[error("Failed to build the HTTP client: {0}")]
Http(#[from] reqwest::Error),
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Request failed: {0}")]
Request(#[from] RequestError),
#[error("Credential refresh failed: {0}")]
CredentialRefresh(#[from] CredentialRefreshError),
#[error("Pkarr operation failed: {0}")]
Pkarr(#[from] PkarrError),
#[error("Failed to parse URL: {0}")]
Parse(#[from] url::ParseError),
#[error("Authentication error: {0}")]
Authentication(#[from] AuthError),
#[error("Client build failed: {0}")]
Build(#[from] BuildError),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CredentialRefreshError {
#[error("Grant credential refresh request failed")]
Request {
status: Option<reqwest::StatusCode>,
},
}
#[derive(Debug, Error)]
pub enum PkarrError {
#[error("DNS operation failed: {0}")]
Dns(#[from] pkarr::dns::SimpleDnsError),
#[error("Failed to build or sign DNS packet: {0}")]
SignPacket(#[from] pkarr::errors::SignedPacketBuildError),
#[error("Failed to publish record to the DHT: {0}")]
Publish(#[from] pkarr::errors::PublishError),
#[error("Failed to resolve the DHT record: {0}")]
Resolve(#[from] pkarr::errors::ResolveError),
#[error("Pkarr record is malformed or missing required data: {0}")]
InvalidRecord(String),
}
impl PkarrError {
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(self, Self::Publish(_) | Self::Resolve(_))
}
}
#[derive(Debug, Error)]
pub enum AuthError {
#[error("Cookie session record handling failed: {0}")]
CookieSessionRecord(#[from] pubky_common::session::Error),
#[error("Token verification failed: {0}")]
VerificationFailed(#[from] pubky_common::auth::Error),
#[error("Cryptography error: {0}")]
DecryptError(#[from] pubky_common::crypto::DecryptError),
#[error("General authentication error: {0}")]
Validation(String),
#[error("The provided auth request has expired or was cancelled.")]
RequestExpired,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum RequestErrorKind {
Http {
status: u16,
},
Timeout,
Connection,
Body,
Decode,
InvalidRequest,
Other,
}
impl RequestErrorKind {
pub fn from_transport(error: &reqwest::Error) -> Self {
if error.is_timeout() {
return Self::Timeout;
}
if let Some(status) = error.status() {
return Self::Http {
status: status.as_u16(),
};
}
#[cfg(not(target_arch = "wasm32"))]
if error.is_connect() {
return Self::Connection;
}
if error.is_body() {
Self::Body
} else if error.is_decode() {
Self::Decode
} else if error.is_builder() {
Self::InvalidRequest
} else {
Self::Other
}
}
}
#[derive(Debug, Error)]
pub enum RequestError {
#[error("HTTP transport error: {0}")]
Transport(#[from] reqwest::Error),
#[error("Server responded with an error: {status} - {message}")]
Server {
status: reqwest::StatusCode,
message: String,
},
#[error("Invalid request/URI: {message}")]
Validation {
message: String,
},
#[error("JSON decode error: {message}")]
DecodeJson {
message: String,
},
}
impl RequestError {
pub fn kind(&self) -> RequestErrorKind {
match self {
Self::Transport(error) => RequestErrorKind::from_transport(error),
Self::Server { status, .. } => RequestErrorKind::Http {
status: status.as_u16(),
},
Self::Validation { .. } => RequestErrorKind::InvalidRequest,
Self::DecodeJson { .. } => RequestErrorKind::Decode,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
macro_rules! impl_from_for_error {
($from_type:ty, $to_variant:path) => {
impl From<$from_type> for Error {
fn from(err: $from_type) -> Self {
$to_variant(err.into())
}
}
};
}
impl_from_for_error!(pkarr::errors::SignedPacketBuildError, Error::Pkarr);
impl_from_for_error!(pkarr::errors::PublishError, Error::Pkarr);
impl_from_for_error!(pkarr::errors::ResolveError, Error::Pkarr);
impl_from_for_error!(pkarr::dns::SimpleDnsError, Error::Pkarr);
impl_from_for_error!(pubky_common::session::Error, Error::Authentication);
impl_from_for_error!(pubky_common::auth::Error, Error::Authentication);
impl_from_for_error!(pubky_common::crypto::DecryptError, Error::Authentication);
impl_from_for_error!(reqwest::Error, Error::Request);
#[cfg(test)]
mod tests {
use super::{RequestError, RequestErrorKind};
#[test]
fn request_categories_do_not_contain_diagnostics() {
for (error, expected) in [
(
RequestError::Server {
status: reqwest::StatusCode::SERVICE_UNAVAILABLE,
message: "private response".into(),
},
RequestErrorKind::Http { status: 503 },
),
(
RequestError::Validation {
message: "private path".into(),
},
RequestErrorKind::InvalidRequest,
),
(
RequestError::DecodeJson {
message: "private payload".into(),
},
RequestErrorKind::Decode,
),
] {
assert_eq!(error.kind(), expected);
assert!(!format!("{:?}", error.kind()).contains("private"));
}
}
}