Skip to main content

rdap_types/
error.rs

1//! Error types for the rdapify library.
2//!
3//! All public-facing errors implement `std::error::Error` via `thiserror`.
4//! The [`RdapError`] enum is the single error type returned by every public API.
5
6use thiserror::Error;
7
8/// The unified error type for all rdapify operations.
9///
10/// # Examples
11///
12/// ```rust
13/// use rdap_types::RdapError;
14///
15/// fn handle(err: RdapError) {
16///     match err {
17///         RdapError::InvalidInput(msg) => eprintln!("Bad input: {msg}"),
18///         RdapError::NoServerFound { query } => eprintln!("No RDAP server for: {query}"),
19///         RdapError::Network(e) => eprintln!("Network error: {e}"),
20///         _ => {}
21///     }
22/// }
23/// ```
24#[derive(Debug, Error)]
25pub enum RdapError {
26    // ── Input validation ──────────────────────────────────────────────────────
27    /// The supplied domain name, IP address, or ASN is not valid.
28    #[error("Invalid input: {0}")]
29    InvalidInput(String),
30
31    // ── SSRF protection ───────────────────────────────────────────────────────
32    /// The resolved URL targets a private, loopback, or link-local address.
33    #[error("SSRF protection blocked request to {url}: {reason}")]
34    SsrfBlocked { url: String, reason: String },
35
36    /// The URL scheme is not HTTPS.
37    #[error("Only HTTPS is allowed, got: {scheme}")]
38    InsecureScheme { scheme: String },
39
40    // ── Bootstrap (IANA server discovery) ────────────────────────────────────
41    /// No RDAP server was found for the given TLD / IP range / ASN range.
42    #[error("No RDAP server found for: {query}")]
43    NoServerFound { query: String },
44
45    /// The IANA bootstrap file could not be fetched or parsed.
46    #[error("Bootstrap fetch failed for {resource}: {source}")]
47    BootstrapFetch {
48        resource: String,
49        #[source]
50        source: Box<RdapError>,
51    },
52
53    // ── Network & HTTP ────────────────────────────────────────────────────────
54    /// A network-level error occurred (DNS, TCP, TLS, timeout).
55    #[error("Network error: {0}")]
56    Network(#[from] reqwest::Error),
57
58    /// The RDAP server returned an HTTP error status.
59    #[error("RDAP server returned HTTP {status} for {url}")]
60    HttpStatus { status: u16, url: String },
61
62    /// The request did not complete within the configured timeout.
63    #[error("Request timed out after {millis}ms: {url}")]
64    Timeout { millis: u64, url: String },
65
66    // ── Response parsing ──────────────────────────────────────────────────────
67    /// The response JSON could not be deserialized into a known RDAP type.
68    #[error("Failed to parse RDAP response: {reason}")]
69    ParseError { reason: String },
70
71    /// The response is missing a required `objectClassName` field.
72    #[error("RDAP response missing objectClassName")]
73    MissingObjectClass,
74
75    /// The response contains an `objectClassName` that this client does not
76    /// recognise.
77    #[error("Unknown RDAP objectClassName: {class}")]
78    UnknownObjectClass { class: String },
79
80    // ── Cache ─────────────────────────────────────────────────────────────────
81    /// An internal cache operation failed (should be rare).
82    #[error("Cache error: {0}")]
83    Cache(String),
84
85    // ── URL utilities ─────────────────────────────────────────────────────────
86    /// A URL could not be parsed.
87    #[error("Invalid URL '{url}': {source}")]
88    InvalidUrl {
89        url: String,
90        #[source]
91        source: url::ParseError,
92    },
93}
94
95impl RdapError {
96    /// Returns an HTTP-like status code for the error, suitable for
97    /// surfacing through FFI or REST bindings.
98    pub fn status_code(&self) -> u16 {
99        match self {
100            RdapError::InvalidInput(_) => 400,
101            RdapError::SsrfBlocked { .. } => 403,
102            RdapError::InsecureScheme { .. } => 403,
103            RdapError::NoServerFound { .. } => 404,
104            RdapError::HttpStatus { status, .. } => *status,
105            RdapError::Timeout { .. } => 408,
106            RdapError::Network(_) => 502,
107            RdapError::BootstrapFetch { .. } => 502,
108            RdapError::ParseError { .. } => 500,
109            RdapError::MissingObjectClass => 500,
110            RdapError::UnknownObjectClass { .. } => 500,
111            RdapError::Cache(_) => 500,
112            RdapError::InvalidUrl { .. } => 400,
113        }
114    }
115
116    /// Returns `true` if the error is caused by invalid user input.
117    pub fn is_invalid_input(&self) -> bool {
118        matches!(self, RdapError::InvalidInput(_))
119    }
120
121    /// Returns `true` if the error is a network-level failure.
122    pub fn is_network(&self) -> bool {
123        matches!(
124            self,
125            RdapError::Network(_) | RdapError::Timeout { .. } | RdapError::HttpStatus { .. }
126        )
127    }
128
129    /// Returns `true` if the request was blocked by SSRF protection.
130    pub fn is_ssrf_blocked(&self) -> bool {
131        matches!(
132            self,
133            RdapError::SsrfBlocked { .. } | RdapError::InsecureScheme { .. }
134        )
135    }
136}
137
138/// Convenience alias used throughout the crate.
139pub type Result<T> = std::result::Result<T, RdapError>;