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}