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