1use std::fmt;
2
3#[derive(Debug)]
8pub enum Error {
9 Authentication {
11 status: u16,
12 message: String,
13 body: String,
14 },
15 RateLimit {
17 status: u16,
18 message: String,
19 body: String,
20 limit: Option<i64>,
21 remaining: Option<i64>,
22 reset: Option<i64>,
23 },
24 InvalidRequest {
26 status: u16,
27 message: String,
28 body: String,
29 },
30 Server {
32 status: u16,
33 message: String,
34 body: String,
35 },
36 Api {
38 status: u16,
39 message: String,
40 body: String,
41 },
42 Transport(reqwest::Error),
44 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}