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