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>;