| 1 | //! Errors surfaced by the sigit.si API client. |
| 2 | |
| 3 | /// The API's error body — `Api::BaseController#render_error` renders every |
| 4 | /// failure as `{ "error_code": <i32>, "message": <string> }`. |
| 5 | #[derive(Debug, Clone, serde::Deserialize)] |
| 6 | pub struct ErrorResponse { |
| 7 | pub error_code: i32, |
| 8 | pub message: String, |
| 9 | } |
| 10 | |
| 11 | /// `error_codes::ErrorCode` values the server sends, mirrored from |
| 12 | /// `Api::BaseController`. |
| 13 | pub mod codes { |
| 14 | pub const UNKNOWN: i32 = 0; |
| 15 | pub const UNAUTHORIZED: i32 = 100; |
| 16 | pub const INVALID: i32 = 101; |
| 17 | } |
| 18 | |
| 19 | pub type Result<T> = std::result::Result<T, ApiError>; |
| 20 | |
| 21 | #[derive(Debug, thiserror::Error)] |
| 22 | pub enum ApiError { |
| 23 | /// The token is missing, expired, or rejected. Callers treat this as |
| 24 | /// "clear the stored token and ask the user to sign in again", so it stays |
| 25 | /// a distinct variant rather than folding into [`ApiError::Status`]. |
| 26 | #[error("Your session has expired. Sign in again with `si auth login`.")] |
| 27 | Unauthorized, |
| 28 | |
| 29 | /// No token on disk for this environment. |
| 30 | #[error("You are not signed in. Run `si auth login` first.")] |
| 31 | NotAuthenticated, |
| 32 | |
| 33 | /// The server answered with a non-2xx status and an `ErrorResponse` body. |
| 34 | #[error("{message}")] |
| 35 | Status { |
| 36 | status: u16, |
| 37 | error_code: i32, |
| 38 | message: String, |
| 39 | }, |
| 40 | |
| 41 | /// An MCP tool reported an in-band failure (`isError: true`). Issues and |
| 42 | /// pull requests go over MCP, so this is a normal user-facing error. |
| 43 | #[error("{0}")] |
| 44 | Tool(String), |
| 45 | |
| 46 | /// The request never completed — DNS, TLS, connection refused, timeout. |
| 47 | #[error("Could not reach {host}: {source}")] |
| 48 | Transport { |
| 49 | host: String, |
| 50 | #[source] |
| 51 | source: reqwest::Error, |
| 52 | }, |
| 53 | |
| 54 | /// A 2xx body that didn't match the expected shape. |
| 55 | #[error("Unexpected response from the server: {0}")] |
| 56 | Parse(String), |
| 57 | } |
| 58 | |
| 59 | impl ApiError { |
| 60 | /// True when the caller should discard the stored credentials. |
| 61 | pub fn is_auth_failure(&self) -> bool { |
| 62 | matches!(self, ApiError::Unauthorized | ApiError::NotAuthenticated) |
| 63 | || matches!(self, ApiError::Status { status: 401, .. }) |
| 64 | } |
| 65 | } |