rust_mc_status/error.rs
1//! Error types for the rust-mc-status library.
2//!
3//! ## Hierarchy
4//!
5//! [`McError`] is the top-level error type. Each variant wraps a sub-error
6//! that categorises the failure more precisely:
7//!
8//! ```text
9//! McError
10//! ├── Network(NetworkError) — DNS lookup, TCP/UDP connect, timeout
11//! ├── Protocol(ProtocolError) — malformed packets, JSON, UTF-8, Base64
12//! ├── Config(ConfigError) — bad address string, invalid port, unknown edition
13//! ├── Io(std::io::Error) — OS-level I/O (file write, socket error)
14//! └── Proxy(ProxyError) — SOCKS5/HTTP proxy failures [feature = "proxy"]
15//! ```
16//!
17//! ## Matching
18//!
19//! Match at the top level for broad handling, or pattern-match into sub-errors
20//! for fine-grained control:
21//!
22//! ```rust,no_run
23//! use rust_mc_status::{McClient, McError, StatusExt};
24//! use rust_mc_status::error::{NetworkError, ProtocolError, ConfigError};
25//!
26//! # #[tokio::main] async fn main() {
27//! let client = McClient::builder().build();
28//! match client.java("mc.hypixel.net").await {
29//! Ok(s) => println!("online: {}", s.display_players()),
30//! // Broad match — catches any network failure
31//! Err(McError::Network(e)) => println!("network: {e}"),
32//! // Specific match — only DNS failures
33//! Err(McError::Network(NetworkError::Dns(msg))) => println!("DNS: {msg}"),
34//! // Config errors are always the caller's fault
35//! Err(McError::Config(ConfigError::InvalidPort(p))) => println!("bad port: {p}"),
36//! Err(e) => println!("other: {e}"),
37//! }
38//! # }
39//! ```
40//!
41//! ## Retry guidance
42//!
43//! | Variant | Retryable? |
44//! |---------|-----------|
45//! | `Network::Timeout` | Yes — server may be temporarily overloaded |
46//! | `Network::Connection` | Yes — transient network hiccup |
47//! | `Network::Dns` | No — unlikely to resolve immediately |
48//! | `Protocol::*` | No — same response on retry |
49//! | `Config::*` | No — fix the input |
50//! | `Proxy::*` | No — fix proxy configuration |
51
52use thiserror::Error;
53
54// ─── McError ─────────────────────────────────────────────────────────────────
55
56/// Top-level error type returned by all fallible operations in this library.
57///
58/// See the [module documentation](self) for matching examples and retry
59/// guidance.
60#[derive(Error, Debug)]
61pub enum McError {
62 /// A network-layer failure: DNS lookup, TCP/UDP connect, or timeout.
63 ///
64 /// See [`NetworkError`] for specific variants.
65 #[error(transparent)]
66 Network(#[from] NetworkError),
67
68 /// A protocol-level failure: the server sent a response that could not
69 /// be parsed.
70 ///
71 /// See [`ProtocolError`] for specific variants.
72 #[error(transparent)]
73 Protocol(#[from] ProtocolError),
74
75 /// An OS-level I/O error, e.g. a file could not be written when saving
76 /// a favicon.
77 #[error("I/O error: {0}")]
78 Io(#[from] std::io::Error),
79
80 /// An invalid configuration value was provided by the caller.
81 ///
82 /// See [`ConfigError`] for specific variants.
83 #[error(transparent)]
84 Config(#[from] ConfigError),
85
86 /// A SOCKS5 or HTTP CONNECT proxy error.
87 ///
88 /// Only produced when the `proxy` feature is enabled.
89 /// See [`ProxyError`] for specific variants.
90 #[cfg(feature = "proxy")]
91 #[error(transparent)]
92 Proxy(#[from] ProxyError),
93}
94
95// ─── NetworkError ─────────────────────────────────────────────────────────────
96
97/// Errors that occur at the network layer before any Minecraft protocol is spoken.
98#[derive(Error, Debug)]
99pub enum NetworkError {
100 /// DNS hostname resolution failed.
101 ///
102 /// Possible causes: hostname does not exist (`NXDOMAIN`), DNS server
103 /// unreachable, SRV lookup timed out.
104 #[error("DNS resolution failed: {0}")]
105 Dns(String),
106
107 /// TCP (Java) or UDP (Bedrock) connection could not be established.
108 ///
109 /// Possible causes: server is offline, firewall blocking the port,
110 /// wrong port number.
111 #[error("Connection failed: {0}")]
112 Connection(String),
113
114 /// The operation did not complete within the configured timeout.
115 ///
116 /// Adjust the timeout via
117 /// [`McClientBuilder::timeout`](crate::McClientBuilder::timeout) or the
118 /// per-request `.timeout()` method on ping builders.
119 #[error("Request timed out")]
120 Timeout,
121}
122
123// ─── ProtocolError ────────────────────────────────────────────────────────────
124
125/// Errors that occur while speaking the Minecraft wire protocol.
126///
127/// These errors indicate the server returned something unexpected —
128/// retrying the same request will almost certainly produce the same result.
129#[derive(Error, Debug)]
130pub enum ProtocolError {
131 /// The server's response did not match the expected packet format.
132 ///
133 /// Possible causes: the server runs a protocol version not supported by
134 /// this library, a network appliance intercepted the connection, or the
135 /// address is not a Minecraft server at all.
136 #[error("Invalid server response: {0}")]
137 InvalidResponse(String),
138
139 /// The JSON payload in the Java status response could not be deserialised.
140 #[error("JSON parsing error: {0}")]
141 Json(#[from] serde_json::Error),
142
143 /// The server's response contained invalid UTF-8 bytes.
144 #[error("UTF-8 conversion error: {0}")]
145 Utf8(#[from] std::string::FromUtf8Error),
146
147 /// Base64 decoding failed, typically when decoding a server favicon.
148 #[error("Base64 decoding error: {0}")]
149 Base64(#[from] base64::DecodeError),
150}
151
152// ─── ConfigError ─────────────────────────────────────────────────────────────
153
154/// Errors caused by invalid caller-provided configuration.
155///
156/// These errors always indicate a bug or user input error — the library cannot
157/// resolve them on its own.
158#[derive(Error, Debug)]
159pub enum ConfigError {
160 /// An unrecognised server edition string was provided.
161 ///
162 /// Valid values for [`ServerEdition::from_str`](crate::ServerEdition):
163 /// `"java"` and `"bedrock"` (case-insensitive).
164 #[error("Invalid edition: {0}")]
165 InvalidEdition(String),
166
167 /// The port component of an address string could not be parsed as `u16`.
168 ///
169 /// Valid port range: `0`–`65535`.
170 #[error("Invalid port: {0}")]
171 InvalidPort(String),
172
173 /// The address string has an unrecognised or malformed format.
174 ///
175 /// Valid formats: `"host"`, `"host:port"`, `"[::1]"`, `"[::1]:port"`.
176 #[error("Invalid address format: {0}")]
177 InvalidAddress(String),
178}
179
180// ─── ProxyError ───────────────────────────────────────────────────────────────
181
182/// Errors related to proxy usage.
183///
184/// Only available when the `proxy` feature is enabled.
185#[cfg(feature = "proxy")]
186#[derive(Error, Debug)]
187pub enum ProxyError {
188 /// The SOCKS5 or HTTP CONNECT proxy rejected or dropped the connection.
189 ///
190 /// Possible causes: wrong credentials, proxy server unreachable, target
191 /// host blocked by the proxy's ACL, or an HTTP 4xx/5xx response.
192 #[error("Proxy error: {0}")]
193 Connection(String),
194
195 /// The configured proxy does not support UDP ASSOCIATE, which is required
196 /// for Bedrock Edition pings (UDP-based RakNet protocol).
197 ///
198 /// Solutions:
199 /// - Use [`ProxyConfig::socks5_with_udp`](crate::proxy::ProxyConfig::socks5_with_udp)
200 /// if your proxy genuinely supports UDP ASSOCIATE.
201 /// - Omit the proxy for Bedrock pings and connect directly.
202 /// - Route UDP at the OS level (e.g. `tun2socks`) instead.
203 #[error("Proxy '{0}' does not support UDP — Bedrock pings require UDP ASSOCIATE")]
204 UdpUnsupported(String),
205}
206
207// ─── Internal convenience constructors ───────────────────────────────────────
208//
209// These are used throughout the library to avoid repeating `.into()` chains.
210// They are `pub(crate)` — callers should match on the public enum variants.
211
212impl McError {
213 /// Construct `McError::Network(NetworkError::Dns(msg))`.
214 #[doc(hidden)]
215 pub fn dns(msg: impl Into<String>) -> Self {
216 NetworkError::Dns(msg.into()).into()
217 }
218 /// Construct `McError::Network(NetworkError::Connection(msg))`.
219 #[doc(hidden)]
220 pub fn connection(msg: impl Into<String>) -> Self {
221 NetworkError::Connection(msg.into()).into()
222 }
223 /// Construct `McError::Network(NetworkError::Timeout)`.
224 #[doc(hidden)]
225 pub fn timeout() -> Self {
226 NetworkError::Timeout.into()
227 }
228 /// Construct `McError::Protocol(ProtocolError::InvalidResponse(msg))`.
229 #[doc(hidden)]
230 pub fn invalid_response(msg: impl Into<String>) -> Self {
231 ProtocolError::InvalidResponse(msg.into()).into()
232 }
233 /// Construct `McError::Config(ConfigError::InvalidPort(msg))`.
234 #[doc(hidden)]
235 pub fn invalid_port(msg: impl Into<String>) -> Self {
236 ConfigError::InvalidPort(msg.into()).into()
237 }
238 /// Construct `McError::Config(ConfigError::InvalidAddress(msg))`.
239 #[doc(hidden)]
240 pub fn invalid_address(msg: impl Into<String>) -> Self {
241 ConfigError::InvalidAddress(msg.into()).into()
242 }
243 /// Construct `McError::Config(ConfigError::InvalidEdition(msg))`.
244 #[doc(hidden)]
245 pub fn invalid_edition(msg: impl Into<String>) -> Self {
246 ConfigError::InvalidEdition(msg.into()).into()
247 }
248 /// Construct `McError::Proxy(ProxyError::Connection(msg))`.
249 #[cfg(feature = "proxy")]
250 #[doc(hidden)]
251 pub fn proxy_error(msg: impl Into<String>) -> Self {
252 ProxyError::Connection(msg.into()).into()
253 }
254 /// Construct `McError::Proxy(ProxyError::UdpUnsupported(addr))`.
255 #[cfg(feature = "proxy")]
256 #[doc(hidden)]
257 pub fn proxy_udp_unsupported(addr: impl Into<String>) -> Self {
258 ProxyError::UdpUnsupported(addr.into()).into()
259 }
260}