Skip to main content

taskfast_client/
errors.rs

1// SPDX-License-Identifier: MIT
2//! Cross-cutting error type for the typed client.
3//!
4//! Variants mirror the orchestrator-centric exit-code buckets used by
5//! `taskfast-cli` (see `map_api_error` in [`crate::client`]): callers branch
6//! on `Auth` to re-credential, `Validation` to surface payload/state issues,
7//! `RateLimited` to back off, `Server`/`Network` to retry, `Decode` to bail.
8
9use std::time::Duration;
10
11use thiserror::Error;
12
13/// Convenience alias for `Result<T, [Error]>` returned by client APIs.
14pub type Result<T> = std::result::Result<T, Error>;
15
16/// Unified error surface for typed-client operations.
17#[derive(Debug, Error)]
18pub enum Error {
19    /// Authentication or authorization failure (HTTP 401/403, bad header bytes,
20    /// etc.). Caller should re-credential, not retry the same payload.
21    #[error("auth: {0}")]
22    Auth(String),
23
24    /// Server rejected the request payload or current state (HTTP 4xx other
25    /// than 401/403/429). `code` is the TaskFast `error` envelope tag (e.g.
26    /// `"validation_error"`); `message` is the human-readable detail.
27    #[error("validation ({code}): {message}")]
28    Validation {
29        /// TaskFast `components/schemas/Error.error` short code.
30        code: String,
31        /// Human-readable detail from the API response body.
32        message: String,
33    },
34
35    /// Server responded 429 Too Many Requests. `retry_after` reflects the
36    /// `Retry-After` header (or a 1-second default when absent/unparseable).
37    #[error("rate limited (retry in {retry_after:?})")]
38    RateLimited {
39        /// Duration callers should sleep before re-issuing the request.
40        retry_after: Duration,
41    },
42
43    /// Server-side failure (HTTP 5xx) or pre-flight client-side error that
44    /// callers should treat as transient + retry-eligible.
45    #[error("server: {0}")]
46    Server(String),
47
48    /// Underlying transport (DNS, connect, TLS, body stream) failed.
49    #[error("network: {0}")]
50    Network(#[from] reqwest::Error),
51
52    /// Response body could not be deserialised into the expected schema.
53    #[error("decode: {0}")]
54    Decode(#[from] serde_json::Error),
55}
56
57impl Error {
58    /// Short, stable variant tag for logging without leaking the inner
59    /// message. A compromised server (or an endpoint that echoes request
60    /// bodies into its error strings) could land an `X-API-Key` value
61    /// inside an `Auth`/`Server` payload — logging the full `Display`
62    /// would then page the secret into any log sink. Callers that need
63    /// logging should prefer [`Self::kind`] over `?err`.
64    pub fn kind(&self) -> &'static str {
65        match self {
66            Self::Auth(_) => "auth",
67            Self::Validation { .. } => "validation",
68            Self::RateLimited { .. } => "rate_limited",
69            Self::Server(_) => "server",
70            Self::Network(_) => "network",
71            Self::Decode(_) => "decode",
72        }
73    }
74}