Skip to main content

loonfs_client/
error.rs

1//! [`ClientError`]: every failure the blocking client surfaces.
2
3use loonfs_api::{ErrorCode, ErrorDetails, ErrorKind};
4use thiserror::Error;
5
6/// Error returned by the blocking HTTP client.
7///
8/// Foreign causes (io, json, ureq) are captured as message strings rather
9/// than `#[source]` chains.
10#[derive(Debug, Error)]
11#[non_exhaustive]
12pub enum ClientError {
13    #[error("failed to read config: {0}")]
14    ConfigIo(String),
15    #[error("failed to decode config: {0}")]
16    ConfigDecode(String),
17    #[error("missing `{field}`")]
18    MissingConfigField { field: &'static str },
19    #[error("invalid `{field}`: {reason}")]
20    ConfigValidation { field: &'static str, reason: String },
21    #[error("invalid namespace path `{0}`")]
22    InvalidNamespacePath(String),
23    #[error("invalid commit_id `{0}`")]
24    InvalidCommitId(String),
25    #[error("invalid checkpoint_id `{0}`")]
26    InvalidCheckpointId(String),
27    #[error("http error: {0}")]
28    Http(String),
29    #[error("server returned {status} {code}: {message}")]
30    Api {
31        status: u16,
32        code: String,
33        /// Capability feature key accompanying `not_supported` errors.
34        feature: Option<String>,
35        message: String,
36        /// Correlation id the server assigned to the failed request.
37        request_id: Option<String>,
38        /// Structured context for the code, when the server sent any. Boxed
39        /// so the rare detailed error does not widen every client result.
40        details: Option<Box<ErrorDetails>>,
41    },
42    #[error("i/o error: {0}")]
43    Io(String),
44    #[error("json error: {0}")]
45    Json(String),
46}
47
48impl ClientError {
49    /// Returns the typed code for [`ClientError::Api`] errors, or `None` for
50    /// non-API errors and for codes this build does not know (clients must
51    /// tolerate unknown codes).
52    pub fn code(&self) -> Option<ErrorCode> {
53        match self {
54            ClientError::Api { code, .. } => ErrorCode::parse(code),
55            _ => None,
56        }
57    }
58
59    /// Returns the caller-action category for [`ClientError::Api`] errors.
60    ///
61    /// Known codes classify through [`ErrorCode::kind`]. Unknown codes (a
62    /// newer server) fall back to the HTTP status class, so retry decisions
63    /// still work: 503 is [`ErrorKind::Unavailable`], other 5xx are
64    /// [`ErrorKind::Internal`], and 4xx are [`ErrorKind::InvalidRequest`].
65    pub fn kind(&self) -> Option<ErrorKind> {
66        match self {
67            ClientError::Api { status, code, .. } => match ErrorCode::parse(code) {
68                Some(code) => Some(code.kind()),
69                None => kind_for_status_class(*status),
70            },
71            _ => None,
72        }
73    }
74}
75
76/// Coarse status-class fallback for error codes this build does not know.
77pub(crate) fn kind_for_status_class(status: u16) -> Option<ErrorKind> {
78    match status {
79        // 503 stays retryable even when the code is unknown.
80        503 => Some(ErrorKind::Unavailable),
81        400..=499 => Some(ErrorKind::InvalidRequest),
82        500..=599 => Some(ErrorKind::Internal),
83        _ => None,
84    }
85}