Skip to main content

posthog_rs/
error.rs

1use std::fmt::{Display, Formatter};
2
3impl std::error::Error for Error {}
4
5impl Display for Error {
6    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
7        match self {
8            Error::Connection(msg) => write!(f, "Connection Error: {msg}"),
9            Error::Serialization(msg) => write!(f, "Serialization Error: {msg}"),
10            Error::AlreadyInitialized => write!(f, "Client already initialized"),
11            Error::NotInitialized => write!(f, "Client not initialized"),
12            Error::PanicHookAlreadyInstalled => write!(f, "Panic hook already installed"),
13            Error::InvalidTimestamp(msg) => write!(f, "Invalid Timestamp: {msg}"),
14            Error::InconclusiveMatch(msg) => write!(f, "Inconclusive Match: {msg}"),
15            Error::RateLimit => write!(f, "Rate limited"),
16            Error::BadRequest(msg) => write!(f, "Bad Request: {msg}"),
17            Error::ServerError { status, message } => {
18                write!(f, "Server Error (HTTP {status}): {message}")
19            }
20            Error::Unauthorized => write!(f, "Unauthorized: invalid or missing API token"),
21            Error::BillingLimitExceeded(msg) => {
22                write!(f, "Billing Limit Exceeded: {msg}")
23            }
24        }
25    }
26}
27
28/// Errors that can occur when using the PostHog client.
29#[derive(Debug)]
30#[non_exhaustive]
31pub enum Error {
32    /// Network or HTTP error when communicating with PostHog API
33    Connection(String),
34    /// Error serializing or deserializing JSON data
35    Serialization(String),
36    /// Global client was already initialized via `init_global`
37    AlreadyInitialized,
38    /// Global client was not initialized before use
39    NotInitialized,
40    /// Panic autocapture hook was already installed
41    PanicHookAlreadyInstalled,
42    /// Timestamp could not be parsed or is invalid
43    InvalidTimestamp(String),
44    /// Flag evaluation was inconclusive (e.g., missing required properties, unknown operator)
45    InconclusiveMatch(String),
46    /// HTTP 429 — the server is rate limiting requests
47    RateLimit,
48    /// HTTP 400 or 413 — the request was malformed or too large
49    BadRequest(String),
50    /// HTTP 5xx — the server encountered an error
51    ServerError {
52        /// HTTP status code returned by the server.
53        status: u16,
54        /// Response body or error message returned by the server.
55        message: String,
56    },
57    /// HTTP 401 — invalid or missing Bearer token
58    Unauthorized,
59    /// HTTP 402 — billing quota exceeded (non-retryable)
60    BillingLimitExceeded(String),
61}
62
63impl Error {
64    /// Construct an error from an HTTP response's status and body.
65    /// Returns `None` for success (2xx) status codes.
66    pub(crate) fn from_http_response(status: u16, body: String) -> Option<Self> {
67        match status {
68            200..=299 => None,
69            401 => Some(Error::Unauthorized),
70            402 => Some(Error::BillingLimitExceeded(body)),
71            429 => Some(Error::RateLimit),
72            400 | 413 => Some(Error::BadRequest(body)),
73            500..=599 => Some(Error::ServerError {
74                status,
75                message: body,
76            }),
77            _ => Some(Error::Connection(format!(
78                "Unexpected HTTP status {status}: {body}"
79            ))),
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn success_returns_none() {
90        assert!(Error::from_http_response(200, String::new()).is_none());
91        assert!(Error::from_http_response(201, String::new()).is_none());
92        assert!(Error::from_http_response(299, String::new()).is_none());
93    }
94
95    #[test]
96    fn rate_limit() {
97        let err = Error::from_http_response(429, String::new()).unwrap();
98        assert!(matches!(err, Error::RateLimit));
99    }
100
101    #[test]
102    fn bad_request_preserves_body() {
103        let err = Error::from_http_response(400, "invalid payload".to_string()).unwrap();
104        match err {
105            Error::BadRequest(msg) => assert_eq!(msg, "invalid payload"),
106            _ => panic!("expected BadRequest"),
107        }
108    }
109
110    #[test]
111    fn payload_too_large() {
112        let err = Error::from_http_response(413, "too large".to_string()).unwrap();
113        match err {
114            Error::BadRequest(msg) => assert_eq!(msg, "too large"),
115            _ => panic!("expected BadRequest for 413"),
116        }
117    }
118
119    #[test]
120    fn server_error_preserves_status_and_body() {
121        let err = Error::from_http_response(503, "unavailable".to_string()).unwrap();
122        match err {
123            Error::ServerError { status, message } => {
124                assert_eq!(status, 503);
125                assert_eq!(message, "unavailable");
126            }
127            _ => panic!("expected ServerError"),
128        }
129    }
130
131    #[test]
132    fn unexpected_status_becomes_connection_error() {
133        let err = Error::from_http_response(302, "redirect".to_string()).unwrap();
134        match err {
135            Error::Connection(msg) => {
136                assert!(msg.contains("302"));
137                assert!(msg.contains("redirect"));
138            }
139            _ => panic!("expected Connection"),
140        }
141    }
142}