Skip to main content

infobip_sms/
error.rs

1//! Error types returned by the SDK.
2//!
3//! Most calls surface [`Error`]. When the server returns a non-2xx
4//! response, the SDK tries to parse the body into one of the two error
5//! schemas the API uses:
6//!
7//! - The richer `ApiError` shape ([`Error::Api`]) — used by the v3
8//!   `/sms/3/*` endpoints. Contains `errorCode`, `description`, `action`,
9//!   plus arrays of [`ApiErrorViolation`]s and [`ApiErrorResource`]s.
10//! - The legacy `ApiException` shape ([`Error::Exception`]) — used by the
11//!   v1 `/sms/1/*` endpoints. Contains a single nested
12//!   `messageId` / `text` pair (and sometimes a map of validation
13//!   errors).
14//!
15//! If neither schema parses, the body is preserved verbatim in
16//! [`Error::Unexpected`] so you can debug it.
17
18use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20
21/// All errors the SDK can produce.
22///
23/// See the [`error` module docs](self) for the dispatch logic between
24/// [`Error::Api`] and [`Error::Exception`].
25#[derive(Debug, thiserror::Error)]
26pub enum Error {
27    /// Returned when [`Client::builder`](crate::Client::builder) is
28    /// missing required state, or when an option is malformed (e.g. an
29    /// auth value that can't be put in an HTTP header).
30    #[error("invalid configuration: {0}")]
31    Config(String),
32
33    /// The configured base URL or a derived endpoint URL failed to
34    /// parse.
35    #[error("invalid URL: {0}")]
36    Url(#[from] url::ParseError),
37
38    /// A network or HTTP-level failure: connection refused, timeout,
39    /// TLS error, mid-stream disconnect, etc.
40    #[error("HTTP transport error: {0}")]
41    Http(#[from] reqwest::Error),
42
43    /// The response body couldn't be (de)serialized as JSON.
44    ///
45    /// You shouldn't see this for normal operation — open an issue if
46    /// you do, since it usually means the API has drifted from the
47    /// version of the spec this crate was built against.
48    #[error("failed to (de)serialize JSON: {0}")]
49    Json(#[from] serde_json::Error),
50
51    /// Structured error returned by the v3 endpoints.
52    ///
53    /// Endpoints that surface this variant: `/sms/3/messages`,
54    /// `/sms/3/reports`, `/sms/3/logs`.
55    #[error("API error ({status}): {error:?}")]
56    Api {
57        /// HTTP status code (e.g. `400`, `401`, `403`, `500`).
58        status: u16,
59        /// Server-provided structured error.
60        error: Box<ApiError>,
61    },
62
63    /// Legacy `ApiException` returned by the v1 endpoints.
64    ///
65    /// Endpoints that surface this variant: `/sms/1/preview`,
66    /// `/sms/1/bulks` (and `.../status`), `/sms/1/inbox/reports`,
67    /// `/ct/1/log/end/{messageId}`.
68    #[error("API exception ({status}): {exception:?}")]
69    Exception {
70        /// HTTP status code.
71        status: u16,
72        /// Server-provided exception payload.
73        exception: Box<ApiException>,
74    },
75
76    /// The server returned a non-2xx response whose body matched
77    /// neither error schema. The raw body is preserved verbatim.
78    #[error("HTTP {status}: {body}")]
79    Unexpected {
80        /// HTTP status code.
81        status: u16,
82        /// Raw response body, decoded as UTF-8 (lossily if needed).
83        body: String,
84    },
85}
86
87/// Structured error envelope used by the v3 endpoints.
88///
89/// Mirrors the API's `ApiError` schema. All fields are optional because
90/// the API does not always populate every field on every error.
91#[derive(Debug, Clone, Default, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ApiError {
94    /// Stable, machine-readable error code (e.g. `BAD_REQUEST`,
95    /// `UNAUTHORIZED`, `RESOURCE_NOT_FOUND`).
96    pub error_code: Option<String>,
97    /// Human-readable description of what went wrong.
98    pub description: Option<String>,
99    /// Suggested next action to recover from the error.
100    pub action: Option<String>,
101    /// Per-field validation failures (e.g. "messages\[0\].destinations
102    /// must not be empty").
103    #[serde(default)]
104    pub violations: Vec<ApiErrorViolation>,
105    /// Documentation links the API thinks may help you recover.
106    #[serde(default)]
107    pub resources: Vec<ApiErrorResource>,
108}
109
110/// One field-level validation failure inside an [`ApiError`].
111#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct ApiErrorViolation {
113    /// Property path that triggered the violation
114    /// (e.g. `messages[0].content.text`).
115    pub property: Option<String>,
116    /// Detailed description of the violation.
117    pub violation: Option<String>,
118}
119
120/// A documentation resource (name + URL) attached to an [`ApiError`].
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122pub struct ApiErrorResource {
123    /// Friendly name of the resource.
124    pub name: Option<String>,
125    /// URL of the resource.
126    pub url: Option<String>,
127}
128
129/// Legacy error envelope used by the v1 endpoints.
130///
131/// Mirrors the API's `ApiException` schema. Wraps a single nested
132/// [`ApiRequestError`] under the `requestError` field.
133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct ApiException {
136    /// The actual error payload, when the server emits one.
137    pub request_error: Option<ApiRequestError>,
138}
139
140/// Inner wrapper for [`ApiException`].
141///
142/// The legacy schema double-wraps every error inside two levels of
143/// envelope; this is the intermediate level.
144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct ApiRequestError {
147    /// Service-specific exception details.
148    pub service_exception: Option<ApiRequestErrorDetails>,
149}
150
151/// Concrete details of a legacy [`ApiException`].
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct ApiRequestErrorDetails {
155    /// Stable, machine-readable error identifier.
156    pub message_id: Option<String>,
157    /// Human-readable error description.
158    pub text: Option<String>,
159    /// Per-field validation failures keyed by property name.
160    #[serde(default)]
161    pub validation_errors: HashMap<String, Vec<String>>,
162}