rust-mc-status 3.0.0

High-performance asynchronous Rust library for querying Minecraft server status (Java & Bedrock)
Documentation
//! Error types for the rust-mc-status library.
//!
//! ## Hierarchy
//!
//! [`McError`] is the top-level error type.  Each variant wraps a sub-error
//! that categorises the failure more precisely:
//!
//! ```text
//! McError
//! ├── Network(NetworkError)   — DNS lookup, TCP/UDP connect, timeout
//! ├── Protocol(ProtocolError) — malformed packets, JSON, UTF-8, Base64
//! ├── Config(ConfigError)     — bad address string, invalid port, unknown edition
//! ├── Io(std::io::Error)      — OS-level I/O (file write, socket error)
//! └── Proxy(ProxyError)       — SOCKS5/HTTP proxy failures  [feature = "proxy"]
//! ```
//!
//! ## Matching
//!
//! Match at the top level for broad handling, or pattern-match into sub-errors
//! for fine-grained control:
//!
//! ```rust,no_run
//! use rust_mc_status::{McClient, McError, StatusExt};
//! use rust_mc_status::error::{NetworkError, ProtocolError, ConfigError};
//!
//! # #[tokio::main] async fn main() {
//! let client = McClient::builder().build();
//! match client.java("mc.hypixel.net").await {
//!     Ok(s)  => println!("online: {}", s.display_players()),
//!     // Broad match — catches any network failure
//!     Err(McError::Network(e)) => println!("network: {e}"),
//!     // Specific match — only DNS failures
//!     Err(McError::Network(NetworkError::Dns(msg))) => println!("DNS: {msg}"),
//!     // Config errors are always the caller's fault
//!     Err(McError::Config(ConfigError::InvalidPort(p))) => println!("bad port: {p}"),
//!     Err(e) => println!("other: {e}"),
//! }
//! # }
//! ```
//!
//! ## Retry guidance
//!
//! | Variant | Retryable? |
//! |---------|-----------|
//! | `Network::Timeout` | Yes — server may be temporarily overloaded |
//! | `Network::Connection` | Yes — transient network hiccup |
//! | `Network::Dns` | No — unlikely to resolve immediately |
//! | `Protocol::*` | No — same response on retry |
//! | `Config::*` | No — fix the input |
//! | `Proxy::*` | No — fix proxy configuration |

use thiserror::Error;

// ─── McError ─────────────────────────────────────────────────────────────────

/// Top-level error type returned by all fallible operations in this library.
///
/// See the [module documentation](self) for matching examples and retry
/// guidance.
#[derive(Error, Debug)]
pub enum McError {
    /// A network-layer failure: DNS lookup, TCP/UDP connect, or timeout.
    ///
    /// See [`NetworkError`] for specific variants.
    #[error(transparent)]
    Network(#[from] NetworkError),

    /// A protocol-level failure: the server sent a response that could not
    /// be parsed.
    ///
    /// See [`ProtocolError`] for specific variants.
    #[error(transparent)]
    Protocol(#[from] ProtocolError),

    /// An OS-level I/O error, e.g. a file could not be written when saving
    /// a favicon.
    #[error("I/O error: {0}")]
    Io(#[from] std::io::Error),

    /// An invalid configuration value was provided by the caller.
    ///
    /// See [`ConfigError`] for specific variants.
    #[error(transparent)]
    Config(#[from] ConfigError),

    /// A SOCKS5 or HTTP CONNECT proxy error.
    ///
    /// Only produced when the `proxy` feature is enabled.
    /// See [`ProxyError`] for specific variants.
    #[cfg(feature = "proxy")]
    #[error(transparent)]
    Proxy(#[from] ProxyError),
}

// ─── NetworkError ─────────────────────────────────────────────────────────────

/// Errors that occur at the network layer before any Minecraft protocol is spoken.
#[derive(Error, Debug)]
pub enum NetworkError {
    /// DNS hostname resolution failed.
    ///
    /// Possible causes: hostname does not exist (`NXDOMAIN`), DNS server
    /// unreachable, SRV lookup timed out.
    #[error("DNS resolution failed: {0}")]
    Dns(String),

    /// TCP (Java) or UDP (Bedrock) connection could not be established.
    ///
    /// Possible causes: server is offline, firewall blocking the port,
    /// wrong port number.
    #[error("Connection failed: {0}")]
    Connection(String),

    /// The operation did not complete within the configured timeout.
    ///
    /// Adjust the timeout via
    /// [`McClientBuilder::timeout`](crate::McClientBuilder::timeout) or the
    /// per-request `.timeout()` method on ping builders.
    #[error("Request timed out")]
    Timeout,
}

// ─── ProtocolError ────────────────────────────────────────────────────────────

/// Errors that occur while speaking the Minecraft wire protocol.
///
/// These errors indicate the server returned something unexpected —
/// retrying the same request will almost certainly produce the same result.
#[derive(Error, Debug)]
pub enum ProtocolError {
    /// The server's response did not match the expected packet format.
    ///
    /// Possible causes: the server runs a protocol version not supported by
    /// this library, a network appliance intercepted the connection, or the
    /// address is not a Minecraft server at all.
    #[error("Invalid server response: {0}")]
    InvalidResponse(String),

    /// The JSON payload in the Java status response could not be deserialised.
    #[error("JSON parsing error: {0}")]
    Json(#[from] serde_json::Error),

    /// The server's response contained invalid UTF-8 bytes.
    #[error("UTF-8 conversion error: {0}")]
    Utf8(#[from] std::string::FromUtf8Error),

    /// Base64 decoding failed, typically when decoding a server favicon.
    #[error("Base64 decoding error: {0}")]
    Base64(#[from] base64::DecodeError),
}

// ─── ConfigError ─────────────────────────────────────────────────────────────

/// Errors caused by invalid caller-provided configuration.
///
/// These errors always indicate a bug or user input error — the library cannot
/// resolve them on its own.
#[derive(Error, Debug)]
pub enum ConfigError {
    /// An unrecognised server edition string was provided.
    ///
    /// Valid values for [`ServerEdition::from_str`](crate::ServerEdition):
    /// `"java"` and `"bedrock"` (case-insensitive).
    #[error("Invalid edition: {0}")]
    InvalidEdition(String),

    /// The port component of an address string could not be parsed as `u16`.
    ///
    /// Valid port range: `0`–`65535`.
    #[error("Invalid port: {0}")]
    InvalidPort(String),

    /// The address string has an unrecognised or malformed format.
    ///
    /// Valid formats: `"host"`, `"host:port"`, `"[::1]"`, `"[::1]:port"`.
    #[error("Invalid address format: {0}")]
    InvalidAddress(String),
}

// ─── ProxyError ───────────────────────────────────────────────────────────────

/// Errors related to proxy usage.
///
/// Only available when the `proxy` feature is enabled.
#[cfg(feature = "proxy")]
#[derive(Error, Debug)]
pub enum ProxyError {
    /// The SOCKS5 or HTTP CONNECT proxy rejected or dropped the connection.
    ///
    /// Possible causes: wrong credentials, proxy server unreachable, target
    /// host blocked by the proxy's ACL, or an HTTP 4xx/5xx response.
    #[error("Proxy error: {0}")]
    Connection(String),

    /// The configured proxy does not support UDP ASSOCIATE, which is required
    /// for Bedrock Edition pings (UDP-based RakNet protocol).
    ///
    /// Solutions:
    /// - Use [`ProxyConfig::socks5_with_udp`](crate::proxy::ProxyConfig::socks5_with_udp)
    ///   if your proxy genuinely supports UDP ASSOCIATE.
    /// - Omit the proxy for Bedrock pings and connect directly.
    /// - Route UDP at the OS level (e.g. `tun2socks`) instead.
    #[error("Proxy '{0}' does not support UDP — Bedrock pings require UDP ASSOCIATE")]
    UdpUnsupported(String),
}

// ─── Internal convenience constructors ───────────────────────────────────────
//
// These are used throughout the library to avoid repeating `.into()` chains.
// They are `pub(crate)` — callers should match on the public enum variants.

impl McError {
    /// Construct `McError::Network(NetworkError::Dns(msg))`.
    #[doc(hidden)]
    pub fn dns(msg: impl Into<String>) -> Self {
        NetworkError::Dns(msg.into()).into()
    }
    /// Construct `McError::Network(NetworkError::Connection(msg))`.
    #[doc(hidden)]
    pub fn connection(msg: impl Into<String>) -> Self {
        NetworkError::Connection(msg.into()).into()
    }
    /// Construct `McError::Network(NetworkError::Timeout)`.
    #[doc(hidden)]
    pub fn timeout() -> Self {
        NetworkError::Timeout.into()
    }
    /// Construct `McError::Protocol(ProtocolError::InvalidResponse(msg))`.
    #[doc(hidden)]
    pub fn invalid_response(msg: impl Into<String>) -> Self {
        ProtocolError::InvalidResponse(msg.into()).into()
    }
    /// Construct `McError::Config(ConfigError::InvalidPort(msg))`.
    #[doc(hidden)]
    pub fn invalid_port(msg: impl Into<String>) -> Self {
        ConfigError::InvalidPort(msg.into()).into()
    }
    /// Construct `McError::Config(ConfigError::InvalidAddress(msg))`.
    #[doc(hidden)]
    pub fn invalid_address(msg: impl Into<String>) -> Self {
        ConfigError::InvalidAddress(msg.into()).into()
    }
    /// Construct `McError::Config(ConfigError::InvalidEdition(msg))`.
    #[doc(hidden)]
    pub fn invalid_edition(msg: impl Into<String>) -> Self {
        ConfigError::InvalidEdition(msg.into()).into()
    }
    /// Construct `McError::Proxy(ProxyError::Connection(msg))`.
    #[cfg(feature = "proxy")]
    #[doc(hidden)]
    pub fn proxy_error(msg: impl Into<String>) -> Self {
        ProxyError::Connection(msg.into()).into()
    }
    /// Construct `McError::Proxy(ProxyError::UdpUnsupported(addr))`.
    #[cfg(feature = "proxy")]
    #[doc(hidden)]
    pub fn proxy_udp_unsupported(addr: impl Into<String>) -> Self {
        ProxyError::UdpUnsupported(addr.into()).into()
    }
}