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}