Skip to main content

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}