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}