Skip to main content

security_client_rs/
error.rs

1use std::{fmt, sync::Arc, time::Duration};
2
3use http::{HeaderMap, StatusCode, header::RETRY_AFTER};
4use serde::{Deserialize, Serialize};
5use serde_json::error::Category;
6use serde_json::{Map, Value};
7
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// One object that failed during an HTTP-successful rule or exception import.
11#[derive(Clone, Debug, Deserialize, Serialize)]
12#[non_exhaustive]
13pub struct ImportFailure {
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub id: Option<String>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub rule_id: Option<String>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub list_id: Option<String>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub item_id: Option<String>,
22    pub error: ImportError,
23    #[serde(flatten)]
24    pub extra: Map<String, Value>,
25}
26
27#[derive(Clone, Debug, Deserialize, Serialize)]
28#[non_exhaustive]
29pub struct ImportError {
30    pub status_code: u16,
31    pub message: String,
32    #[serde(flatten)]
33    pub extra: Map<String, Value>,
34}
35
36/// Responses that fail keep their status and headers. Bodies may contain
37/// operational data, so neither `Display` nor `Debug` prints them, nor any
38/// response value quoted by a decoding error; read [`Error::body`] or
39/// [`DecodeError::inner`] deliberately.
40#[derive(thiserror::Error)]
41#[non_exhaustive]
42pub enum Error {
43    #[error("invalid client configuration: {0}")]
44    Configuration(String),
45    #[error("invalid request: {0}")]
46    InvalidRequest(String),
47    #[error("HTTP transport failed: {0}")]
48    Transport(#[source] TransportError),
49    #[error("Kibana returned HTTP {status}")]
50    #[non_exhaustive]
51    Api {
52        status: StatusCode,
53        headers: Box<HeaderMap>,
54        /// At most 16 KiB of the response body.
55        body: String,
56        /// The body was longer than the retained part.
57        truncated: bool,
58        /// Reading the body failed after the status and headers arrived;
59        /// `body` holds what was received.
60        body_error: Option<TransportError>,
61    },
62    /// A successful status and headers arrived, but reading the body failed.
63    /// For a mutation this is evidence that Kibana accepted the request, not
64    /// proof of its outcome; reconcile by reading the resource back.
65    #[error("reading the body of a Kibana response with HTTP {status} failed: {source}")]
66    #[non_exhaustive]
67    Body {
68        status: StatusCode,
69        headers: Box<HeaderMap>,
70        /// Bytes received before the failure.
71        received: usize,
72        #[source]
73        source: TransportError,
74    },
75    #[error("could not decode Kibana response with HTTP {status}: {source}")]
76    #[non_exhaustive]
77    Decode {
78        status: StatusCode,
79        headers: Box<HeaderMap>,
80        /// At most 16 KiB of the response body.
81        body: String,
82        #[source]
83        source: DecodeError,
84    },
85    #[error("response with HTTP {status} exceeded the configured limit of {limit} bytes")]
86    #[non_exhaustive]
87    ResponseTooLarge {
88        limit: usize,
89        status: StatusCode,
90        headers: Box<HeaderMap>,
91    },
92    /// A page stream received a different page than it requested.
93    #[error("requested page {requested} but Kibana returned page {returned}")]
94    UnexpectedPage { requested: u32, returned: u32 },
95    /// A bounded page stream read its maximum number of pages while more remained.
96    #[error("stopped after {max_pages} pages before the end of the collection")]
97    PageLimit { max_pages: u32 },
98    #[error("request serialization failed: {0}")]
99    Serialize(#[source] Arc<dyn std::error::Error + Send + Sync>),
100}
101
102impl Error {
103    /// The HTTP status of a response that arrived, including successful
104    /// responses whose body could not be read or decoded.
105    pub fn status(&self) -> Option<StatusCode> {
106        match self {
107            Self::Api { status, .. }
108            | Self::Body { status, .. }
109            | Self::Decode { status, .. }
110            | Self::ResponseTooLarge { status, .. } => Some(*status),
111            _ => None,
112        }
113    }
114
115    /// The headers of a response that arrived.
116    pub fn headers(&self) -> Option<&HeaderMap> {
117        match self {
118            Self::Api { headers, .. }
119            | Self::Body { headers, .. }
120            | Self::Decode { headers, .. }
121            | Self::ResponseTooLarge { headers, .. } => Some(headers),
122            _ => None,
123        }
124    }
125
126    /// The retained body excerpt for API or decode errors, limited to 16 KiB.
127    /// Interrupted reads and size-limit errors retain context but no body excerpt.
128    pub fn body(&self) -> Option<&str> {
129        match self {
130            Self::Api { body, .. } | Self::Decode { body, .. } => Some(body),
131            _ => None,
132        }
133    }
134
135    /// The connection, timeout or streaming failure behind this error, whether
136    /// it happened before a response arrived or while reading its body.
137    pub fn transport(&self) -> Option<&TransportError> {
138        match self {
139            Self::Transport(error) | Self::Body { source: error, .. } => Some(error),
140            Self::Api { body_error, .. } => body_error.as_ref(),
141            _ => None,
142        }
143    }
144
145    /// The `message` field of a Kibana JSON error body, when present.
146    pub fn message(&self) -> Option<String> {
147        let Self::Api { body, .. } = self else {
148            return None;
149        };
150        let value: serde_json::Value = serde_json::from_str(body).ok()?;
151        value.get("message")?.as_str().map(str::to_owned)
152    }
153
154    /// The `Retry-After` delay of an error response, when given in seconds.
155    /// HTTP-date values are not parsed.
156    pub fn retry_after(&self) -> Option<Duration> {
157        let Self::Api { headers, .. } = self else {
158            return None;
159        };
160        let seconds = headers
161            .get(RETRY_AFTER)?
162            .to_str()
163            .ok()?
164            .trim()
165            .parse()
166            .ok()?;
167        Some(Duration::from_secs(seconds))
168    }
169
170    pub(crate) fn serialize(error: impl std::error::Error + Send + Sync + 'static) -> Self {
171        Self::Serialize(Arc::new(error))
172    }
173}
174
175impl From<reqwest::Error> for Error {
176    fn from(error: reqwest::Error) -> Self {
177        Self::Transport(TransportError::from(error))
178    }
179}
180
181impl fmt::Debug for Error {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        let names = |headers: &HeaderMap| headers.keys().cloned().collect::<Vec<_>>();
184        match self {
185            Self::Configuration(message) => f.debug_tuple("Configuration").field(message).finish(),
186            Self::InvalidRequest(message) => {
187                f.debug_tuple("InvalidRequest").field(message).finish()
188            }
189            Self::Transport(error) => f.debug_tuple("Transport").field(error).finish(),
190            Self::Api {
191                status,
192                headers,
193                body,
194                truncated,
195                body_error,
196            } => f
197                .debug_struct("Api")
198                .field("status", status)
199                .field("headers", &names(headers))
200                .field("body_bytes", &body.len())
201                .field("truncated", truncated)
202                .field("body_error", body_error)
203                .finish(),
204            Self::Body {
205                status,
206                headers,
207                received,
208                source,
209            } => f
210                .debug_struct("Body")
211                .field("status", status)
212                .field("headers", &names(headers))
213                .field("received", received)
214                .field("source", source)
215                .finish(),
216            Self::Decode {
217                status,
218                headers,
219                body,
220                source,
221            } => f
222                .debug_struct("Decode")
223                .field("status", status)
224                .field("headers", &names(headers))
225                .field("body_bytes", &body.len())
226                .field("source", source)
227                .finish(),
228            Self::ResponseTooLarge {
229                limit,
230                status,
231                headers,
232            } => f
233                .debug_struct("ResponseTooLarge")
234                .field("limit", limit)
235                .field("status", status)
236                .field("headers", &names(headers))
237                .finish(),
238            Self::UnexpectedPage {
239                requested,
240                returned,
241            } => f
242                .debug_struct("UnexpectedPage")
243                .field("requested", requested)
244                .field("returned", returned)
245                .finish(),
246            Self::PageLimit { max_pages } => f
247                .debug_struct("PageLimit")
248                .field("max_pages", max_pages)
249                .finish(),
250            Self::Serialize(error) => f.debug_tuple("Serialize").field(error).finish(),
251        }
252    }
253}
254
255/// A response body that did not match the expected type.
256///
257/// Serde messages can quote response values, such as an unknown enum variant,
258/// so `Display` and `Debug` show only the category and position, and the
259/// serde error is not exposed as an error source. [`inner`](Self::inner)
260/// returns it deliberately.
261pub struct DecodeError(serde_json::Error);
262
263impl DecodeError {
264    pub(crate) fn new(error: serde_json::Error) -> Self {
265        Self(error)
266    }
267
268    /// Whether the body was malformed JSON, ended early, or was valid JSON of the wrong shape.
269    pub fn category(&self) -> Category {
270        self.0.classify()
271    }
272
273    pub fn classify(&self) -> Category {
274        self.category()
275    }
276
277    pub fn line(&self) -> usize {
278        self.0.line()
279    }
280
281    pub fn column(&self) -> usize {
282        self.0.column()
283    }
284
285    /// The full serde error, whose message can contain response values.
286    pub fn inner(&self) -> &serde_json::Error {
287        &self.0
288    }
289
290    /// The full serde error, whose message can contain response values.
291    pub fn as_serde_error(&self) -> &serde_json::Error {
292        self.inner()
293    }
294}
295
296impl fmt::Display for DecodeError {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        let category = match self.category() {
299            Category::Io => "I/O error",
300            Category::Syntax => "invalid JSON",
301            Category::Data => "unexpected JSON content",
302            Category::Eof => "unexpected end of JSON",
303        };
304        write!(
305            f,
306            "{category} at line {} column {}",
307            self.line(),
308            self.column()
309        )
310    }
311}
312
313impl fmt::Debug for DecodeError {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        f.debug_struct("DecodeError")
316            .field("category", &self.category())
317            .field("line", &self.line())
318            .field("column", &self.column())
319            .finish()
320    }
321}
322
323impl std::error::Error for DecodeError {}
324
325/// Query values can carry filters or tokens, so they are dropped from the retained URL.
326impl From<reqwest::Error> for TransportError {
327    fn from(mut error: reqwest::Error) -> Self {
328        if let Some(url) = error.url_mut() {
329            url.set_query(None);
330        }
331        Self(error)
332    }
333}
334
335/// A connection, TLS, timeout, or body-streaming failure.
336pub struct TransportError(reqwest::Error);
337
338impl TransportError {
339    pub fn is_timeout(&self) -> bool {
340        self.0.is_timeout()
341    }
342
343    /// True when the connection failed before a request was sent, including TLS failures.
344    pub fn is_connect(&self) -> bool {
345        self.0.is_connect()
346    }
347}
348
349impl fmt::Debug for TransportError {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        fmt::Debug::fmt(&self.0, f)
352    }
353}
354
355impl fmt::Display for TransportError {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        fmt::Display::fmt(&self.0, f)
358    }
359}
360
361impl std::error::Error for TransportError {
362    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
363        self.0.source()
364    }
365}
366
367/// A request construction failure kept until `send`. Clones of a request share
368/// it, and every clone reports the same variant and source.
369#[derive(Clone)]
370pub(crate) enum Deferred {
371    InvalidRequest(String),
372    Serialize(Arc<dyn std::error::Error + Send + Sync>),
373}
374
375impl From<Error> for Deferred {
376    fn from(error: Error) -> Self {
377        match error {
378            Error::Serialize(source) => Self::Serialize(source),
379            Error::InvalidRequest(message) => Self::InvalidRequest(message),
380            // Construction only produces the two variants above.
381            other => Self::InvalidRequest(other.to_string()),
382        }
383    }
384}
385
386impl From<Deferred> for Error {
387    fn from(error: Deferred) -> Self {
388        match error {
389            Deferred::InvalidRequest(message) => Self::InvalidRequest(message),
390            Deferred::Serialize(source) => Self::Serialize(source),
391        }
392    }
393}