Skip to main content

ipwhois/
error.rs

1//! Error type returned by every fallible operation in this crate.
2//!
3//! The library does not intentionally panic — every failure (network
4//! outage, rate limit, bad IP, bad options, …) flows through the
5//! [`Error`] enum below. Three categories cover the failure modes
6//! (`api`, `network`, `invalid_argument`); each variant carries a
7//! human-readable `message`, and API errors additionally carry HTTP
8//! status and a free-plan `Retry-After` value when available.
9
10use thiserror::Error as ThisError;
11
12/// All errors returned by this crate.
13///
14/// Every variant carries a human-readable `message`. API errors additionally
15/// carry the upstream HTTP status code, and HTTP 429 responses on the **free
16/// plan** also carry the `Retry-After` value the API asked us to honour
17/// (the paid endpoint does not send the header).
18///
19/// Match on the variant when you want to branch on the category, or call
20/// [`Error::error_type`] to get a stable category string
21/// (`"api"`, `"network"`, `"invalid_argument"`).
22#[derive(Debug, Clone, ThisError)]
23#[non_exhaustive]
24pub enum Error {
25    /// The ipwhois.io API itself returned an error. Covers HTTP 4xx / 5xx
26    /// responses, malformed JSON bodies, and HTTP 2xx responses where the
27    /// API sets `success: false` (e.g. "Invalid IP address",
28    /// "Reserved range").
29    #[error("{message}")]
30    Api {
31        /// Human-readable description from the API or synthesised by the
32        /// client when the body is unparseable.
33        message: String,
34        /// HTTP status code, when the failure was an HTTP 4xx / 5xx.
35        /// Absent for `success: false` bodies returned with HTTP 2xx.
36        http_status: Option<u16>,
37        /// Value of the `Retry-After` header, in seconds. Only present on
38        /// HTTP 429 responses from the **free plan** (`ipwho.is`); the paid
39        /// endpoint (`ipwhois.pro`) does not send the header.
40        retry_after: Option<u64>,
41    },
42
43    /// Transport-level failure: DNS, connection refused, TLS handshake,
44    /// timeout, etc. The request never reached the API meaningfully.
45    #[error("Network error: {message}")]
46    Network {
47        /// Description of the underlying transport failure.
48        message: String,
49    },
50
51    /// The caller supplied an invalid argument before any request was made
52    /// (unsupported language, empty bulk list, more than 100 IPs, …).
53    #[error("Invalid argument: {message}")]
54    InvalidArgument {
55        /// Human-readable description of what was wrong.
56        message: String,
57    },
58}
59
60impl Error {
61    /// Stable string identifying the category of this error.
62    ///
63    /// Returns one of: `"api"`, `"network"`, or `"invalid_argument"`.
64    pub fn error_type(&self) -> &'static str {
65        match self {
66            Error::Api { .. } => "api",
67            Error::Network { .. } => "network",
68            Error::InvalidArgument { .. } => "invalid_argument",
69        }
70    }
71
72    /// Human-readable message for this error.
73    pub fn message(&self) -> &str {
74        match self {
75            Error::Api { message, .. } => message,
76            Error::Network { message } => message,
77            Error::InvalidArgument { message } => message,
78        }
79    }
80
81    /// HTTP status code, if this is an [`Error::Api`] variant carrying one.
82    pub fn http_status(&self) -> Option<u16> {
83        match self {
84            Error::Api { http_status, .. } => *http_status,
85            _ => None,
86        }
87    }
88
89    /// Value of the `Retry-After` header in seconds, if available.
90    /// Only present on HTTP 429 responses from the free plan.
91    pub fn retry_after(&self) -> Option<u64> {
92        match self {
93            Error::Api { retry_after, .. } => *retry_after,
94            _ => None,
95        }
96    }
97
98    pub(crate) fn invalid_argument(msg: impl Into<String>) -> Self {
99        Error::InvalidArgument {
100            message: msg.into(),
101        }
102    }
103
104    pub(crate) fn network(msg: impl Into<String>) -> Self {
105        Error::Network {
106            message: msg.into(),
107        }
108    }
109
110    pub(crate) fn api(
111        message: impl Into<String>,
112        http_status: Option<u16>,
113        retry_after: Option<u64>,
114    ) -> Self {
115        Error::Api {
116            message: message.into(),
117            http_status,
118            retry_after,
119        }
120    }
121}
122
123impl From<reqwest::Error> for Error {
124    fn from(e: reqwest::Error) -> Self {
125        Error::Network {
126            message: e.to_string(),
127        }
128    }
129}