armada_client/error.rs
1use thiserror::Error;
2
3/// All errors that can be returned by the Armada client.
4///
5/// Most callers can pattern-match on the variant that matters to them and
6/// propagate the rest with `?`. Use [`Error::auth`] as a convenience
7/// constructor when implementing [`crate::auth::TokenProvider`].
8///
9/// # Example
10///
11/// ```no_run
12/// use armada_client::{ArmadaClient, Error, StaticTokenProvider};
13///
14/// async fn submit_or_retry(client: &ArmadaClient) {
15/// // ... build request ...
16/// # let request = armada_client::JobSubmitRequest {
17/// # queue: "q".into(), job_set_id: "js".into(), job_request_items: vec![],
18/// # };
19/// match client.submit(request).await {
20/// Ok(resp) => { /* handle success */ }
21/// Err(Error::Grpc(status)) => {
22/// // The server returned a non-OK gRPC status (e.g. NOT_FOUND).
23/// eprintln!("gRPC error: {status}");
24/// }
25/// Err(Error::Auth(msg)) => {
26/// // Token provider failed — check credentials.
27/// eprintln!("auth failure: {msg}");
28/// }
29/// Err(e) => eprintln!("other error: {e}"),
30/// }
31/// }
32/// ```
33#[derive(Debug, Error)]
34pub enum Error {
35 /// A low-level transport failure occurred before a gRPC response could be
36 /// received — for example a connection refused, TLS handshake error, or
37 /// DNS resolution failure.
38 #[error("transport error: {0}")]
39 Transport(#[from] tonic::transport::Error),
40
41 /// The server returned a non-OK gRPC status code.
42 ///
43 /// Common codes:
44 /// - `NOT_FOUND` — the requested queue or job set does not exist.
45 /// - `PERMISSION_DENIED` — the bearer token lacks the required privileges.
46 /// - `INVALID_ARGUMENT` — the request payload was rejected by the server.
47 #[error("gRPC error: {0}")]
48 Grpc(#[source] Box<tonic::Status>),
49
50 /// Returned by custom [`crate::auth::TokenProvider`] implementations
51 /// to signal that token retrieval failed.
52 #[error("auth error: {0}")]
53 Auth(String),
54
55 /// Returned when the endpoint string passed to
56 /// [`crate::client::ArmadaClient::connect`] or
57 /// [`crate::client::ArmadaClient::connect_tls`] is not a valid URI.
58 #[error("invalid endpoint URI: {0}")]
59 InvalidUri(String),
60
61 /// The bearer token string contained characters that are not valid in an
62 /// HTTP header value. Token strings must be ASCII and must not contain
63 /// control characters (`\0`, `\r`, `\n`, etc.).
64 #[error("invalid metadata value: {0}")]
65 InvalidMetadata(#[from] tonic::metadata::errors::InvalidMetadataValue),
66}
67
68impl From<tonic::Status> for Error {
69 fn from(s: tonic::Status) -> Self {
70 Self::Grpc(Box::new(s))
71 }
72}
73
74impl Error {
75 /// Convenience constructor for [`Error::Auth`].
76 ///
77 /// Use this in custom [`crate::auth::TokenProvider`] implementations:
78 /// ```ignore
79 /// return Err(Error::auth("token expired"));
80 /// ```
81 pub fn auth(msg: impl Into<String>) -> Self {
82 Self::Auth(msg.into())
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn from_status() {
92 let status = tonic::Status::not_found("not found");
93 let err = Error::from(status);
94 assert!(matches!(err, Error::Grpc(_)));
95 }
96
97 #[test]
98 fn from_invalid_metadata() {
99 let bad: Result<tonic::metadata::MetadataValue<tonic::metadata::Ascii>, _> = "\x00".parse();
100 let err = Error::from(bad.unwrap_err());
101 assert!(matches!(err, Error::InvalidMetadata(_)));
102 }
103
104 #[test]
105 fn invalid_uri_holds_message() {
106 let err = Error::InvalidUri("not a uri".to_string());
107 assert!(err.to_string().contains("not a uri"));
108 }
109
110 #[test]
111 fn auth_constructor() {
112 let err = Error::auth("token expired");
113 assert!(matches!(err, Error::Auth(_)));
114 assert_eq!(err.to_string(), "auth error: token expired");
115 }
116}