Skip to main content

matomo/
error.rs

1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, Error>;
4
5#[non_exhaustive]
6#[derive(Debug, Error)]
7pub enum Error {
8    /// Transport-level failure from the bundled `reqwest` transport. Only
9    /// present with the `reqwest` feature; an accepted semver coupling.
10    #[cfg(feature = "reqwest")]
11    #[error("http transport error: {0}")]
12    Http(#[from] reqwest::Error),
13
14    /// Matomo returned `{"result":"error", ...}` with HTTP 200.
15    #[error("matomo api error in {method}: {message}")]
16    Api {
17        message: String,
18        method: &'static str,
19        kind: ApiErrorKind,
20    },
21
22    /// The body was valid JSON but did not match the expected typed shape.
23    #[error("failed to decode {method} response: {source}")]
24    Decode {
25        #[source]
26        source: serde_json::Error,
27        method: &'static str,
28    },
29
30    /// The body was not JSON at all (e.g. an HTML error page).
31    #[error("non-json body from {method}: {body}")]
32    NonJsonBody { method: &'static str, body: String },
33
34    /// Misconfiguration detected while building the client.
35    #[error("configuration error: {0}")]
36    Config(String),
37
38    /// A preflight check failed.
39    #[error("preflight failed: {0}")]
40    Preflight(String),
41}
42
43/// Best-effort classification of an API error, sniffed from the message text.
44/// This is a heuristic; Matomo does not return machine-readable error codes.
45#[non_exhaustive]
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ApiErrorKind {
48    Auth,
49    UnknownMethod,
50    InvalidParam,
51    RateLimited,
52    NoData,
53    Other,
54}
55
56impl ApiErrorKind {
57    pub(crate) fn classify(message: &str) -> Self {
58        let m = message.to_ascii_lowercase();
59        if m.contains("token_auth")
60            || m.contains("authentication")
61            || m.contains("not allowed")
62            || m.contains("permission")
63        {
64            ApiErrorKind::Auth
65        } else if m.contains("method") && (m.contains("not exist") || m.contains("not found")) {
66            ApiErrorKind::UnknownMethod
67        } else if m.contains("requires") || m.contains("parameter") || m.contains("invalid") {
68            ApiErrorKind::InvalidParam
69        } else if m.contains("rate") && m.contains("limit") {
70            ApiErrorKind::RateLimited
71        } else if m.contains("no data") {
72            ApiErrorKind::NoData
73        } else {
74            ApiErrorKind::Other
75        }
76    }
77}