Skip to main content

pcs_external/
error.rs

1use ppoppo_sdk_core::token_cache::TokenCacheError;
2use tonic::Code;
3
4/// All errors returned by [`crate::PcsExternalClient`] methods.
5///
6/// The three `Grpc*` variants mirror `PasFailure` semantics — `Rejected`
7/// means the call reached PCS and was turned down at the application layer
8/// (don't retry as-is), `ServerError` is a 5xx-class state (retry-eligible),
9/// `Transport` means the call never reached PCS.
10#[derive(Debug, thiserror::Error)]
11#[non_exhaustive]
12pub enum Error {
13    /// Connection, TLS, or network-level failure.
14    #[error("transport error: {0}")]
15    Transport(String),
16
17    /// Bearer token acquisition failed before the gRPC call could be made.
18    #[error("token refresh failed: {0}")]
19    TokenRefresh(#[from] TokenCacheError),
20
21    /// The configured path prefix is malformed — caught at build time.
22    #[error("invalid path prefix '{prefix}': {reason}")]
23    InvalidPathPrefix { prefix: String, reason: String },
24
25    /// PCS returned a non-OK gRPC status at the application layer
26    /// (`InvalidArgument`, `NotFound`, `PermissionDenied`, `Unauthenticated`,
27    /// `ResourceExhausted`, `FailedPrecondition`, `AlreadyExists`,
28    /// `OutOfRange`, `Aborted`, `Cancelled`).
29    ///
30    /// Caller's input, auth, or state is bad — do not retry as-is.
31    #[error("rejected by PCS: {code:?} {message}")]
32    Rejected { code: Code, message: String },
33
34    /// PCS returned a 5xx-class status (`Internal`, `Unknown`, `DataLoss`,
35    /// `Unimplemented`, `DeadlineExceeded`). Retry-eligible.
36    #[error("PCS server error: {code:?} {message}")]
37    ServerError { code: Code, message: String },
38
39    /// A required proto field was absent or could not be mapped to a domain type.
40    #[error("unexpected proto response: {0}")]
41    ProtoMismatch(String),
42}
43
44/// Classify a `tonic::Status` into an [`Error`] variant.
45#[must_use]
46pub(crate) fn classify_status(status: &tonic::Status) -> Error {
47    let code = status.code();
48    let message = status.message().to_string();
49    match code {
50        Code::InvalidArgument
51        | Code::NotFound
52        | Code::AlreadyExists
53        | Code::PermissionDenied
54        | Code::Unauthenticated
55        | Code::ResourceExhausted
56        | Code::FailedPrecondition
57        | Code::OutOfRange
58        | Code::Aborted
59        | Code::Cancelled => Error::Rejected { code, message },
60
61        Code::Internal | Code::Unknown | Code::DataLoss | Code::Unimplemented => {
62            Error::ServerError { code, message }
63        }
64
65        // DeadlineExceeded: most often upstream overload → retry-eligible.
66        Code::DeadlineExceeded => Error::ServerError { code, message },
67
68        // Unavailable is the canonical transient-transport code.
69        Code::Unavailable => Error::Transport(message),
70
71        // Ok shouldn't reach the classifier; degrade gracefully.
72        Code::Ok => Error::Transport(format!("classify_status called on Code::Ok: {message}")),
73    }
74}