Skip to main content

ip_api_io/
error.rs

1use std::fmt;
2
3/// Errors returned by the ip-api.io client.
4///
5/// The client never retries; [`Error::RateLimit`]'s `reset` field is the unix
6/// timestamp when the quota renews.
7#[derive(Debug)]
8pub enum Error {
9    /// HTTP 401/403 — missing or invalid API key.
10    Authentication {
11        status: u16,
12        message: String,
13        body: String,
14    },
15    /// HTTP 429 — quota exhausted, with the x-ratelimit-* header values.
16    RateLimit {
17        status: u16,
18        message: String,
19        body: String,
20        limit: Option<i64>,
21        remaining: Option<i64>,
22        reset: Option<i64>,
23    },
24    /// HTTP 400/404/422 — malformed input or unknown resource.
25    InvalidRequest {
26        status: u16,
27        message: String,
28        body: String,
29    },
30    /// HTTP 5xx — ip-api.io server-side failure.
31    Server {
32        status: u16,
33        message: String,
34        body: String,
35    },
36    /// Any other non-2xx response.
37    Api {
38        status: u16,
39        message: String,
40        body: String,
41    },
42    /// Transport-level failure (DNS, connect, timeout, TLS, JSON decoding).
43    Transport(reqwest::Error),
44    /// Invalid argument detected before any network call (e.g. oversized batch).
45    InvalidArgument(String),
46}
47
48impl fmt::Display for Error {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            Error::Authentication { status, message, .. }
52            | Error::InvalidRequest { status, message, .. }
53            | Error::Server { status, message, .. }
54            | Error::Api { status, message, .. } => {
55                write!(f, "ip-api.io: {message} (HTTP {status})")
56            }
57            Error::RateLimit {
58                status,
59                message,
60                limit,
61                remaining,
62                reset,
63                ..
64            } => write!(
65                f,
66                "ip-api.io: {message} (HTTP {status}, limit={limit:?}, remaining={remaining:?}, reset={reset:?})"
67            ),
68            Error::Transport(inner) => write!(f, "ip-api.io: transport error: {inner}"),
69            Error::InvalidArgument(message) => write!(f, "ip-api.io: {message}"),
70        }
71    }
72}
73
74impl std::error::Error for Error {
75    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
76        match self {
77            Error::Transport(inner) => Some(inner),
78            _ => None,
79        }
80    }
81}
82
83impl From<reqwest::Error> for Error {
84    fn from(inner: reqwest::Error) -> Self {
85        Error::Transport(inner)
86    }
87}
88
89pub(crate) fn classify(status: u16, message: String, body: String) -> Error {
90    match status {
91        401 | 403 => Error::Authentication {
92            status,
93            message,
94            body,
95        },
96        400 | 404 | 422 => Error::InvalidRequest {
97            status,
98            message,
99            body,
100        },
101        500..=599 => Error::Server {
102            status,
103            message,
104            body,
105        },
106        _ => Error::Api {
107            status,
108            message,
109            body,
110        },
111    }
112}
113
114pub(crate) fn extract_message(status: u16, body: &str) -> String {
115    let message = match serde_json::from_str::<serde_json::Value>(body) {
116        Ok(serde_json::Value::Object(map)) => map
117            .get("message")
118            .or_else(|| map.get("error"))
119            .and_then(|value| value.as_str())
120            .unwrap_or_default()
121            .to_string(),
122        Ok(_) => String::new(),
123        Err(_) => body.trim().chars().take(200).collect(),
124    };
125    if message.is_empty() {
126        format!("HTTP {status} from ip-api.io")
127    } else {
128        message
129    }
130}