Skip to main content

esphome_native_api/
error.rs

1//! Error types for the ESPHome native API.
2//!
3//! The API distinguishes errors by *what a consumer would do differently* about
4//! them rather than by where they occurred:
5//!
6//! - [`Error::Disconnected`] — the session ended. This is the normal way a
7//!   connection terminates; most consumers treat it as informational, not a
8//!   failure.
9//! - [`FrameError::UnknownMessageType`] — a valid frame carrying a message type
10//!   this build does not know (e.g. a newer ESPHome release). It is safe to skip
11//!   and keep the connection alive; the read loop does exactly that internally.
12//! - Every other [`FrameError`] / [`HandshakeError`] / [`Error::Io`] variant is a
13//!   genuine fault that tears the connection down.
14
15use std::io;
16
17use thiserror::Error;
18
19/// Top-level error returned by the ESPHome native API.
20#[derive(Debug, Error)]
21#[non_exhaustive]
22pub enum Error {
23    /// The connection ended. This is an expected lifecycle event, not
24    /// necessarily a failure — inspect the [`DisconnectReason`] to decide.
25    #[error("client disconnected ({0})")]
26    Disconnected(DisconnectReason),
27
28    /// A frame could not be turned into a message. The connection is no longer
29    /// in a known state and is torn down.
30    #[error(transparent)]
31    Frame(#[from] FrameError),
32
33    /// Connection establishment or the encryption handshake failed.
34    #[error(transparent)]
35    Handshake(#[from] HandshakeError),
36
37    /// The API was misconfigured (for example, an encryption key that is not
38    /// valid base64).
39    #[error("invalid configuration: {0}")]
40    Config(String),
41
42    /// An unexpected I/O error that is not a normal disconnect.
43    #[error("io error: {0}")]
44    Io(#[from] io::Error),
45
46    /// The connection's background task terminated without reporting an
47    /// outcome (for example, it panicked). The session is over, but nothing is
48    /// known about how it ended.
49    #[error("connection task terminated unexpectedly")]
50    TaskFailed,
51}
52
53/// Why a connection ended.
54///
55/// Every variant is "expected" in the sense that a peer is always allowed to go
56/// away; they are kept distinct so a consumer can log or react differently (for
57/// example, treating [`DisconnectReason::Requested`] as fully graceful).
58#[derive(Debug, Clone, Error)]
59#[non_exhaustive]
60pub enum DisconnectReason {
61    /// The stream reached end of file (the peer closed its side cleanly).
62    #[error("end of stream")]
63    Eof,
64
65    /// The peer reset the connection abruptly (`ConnectionReset`, `BrokenPipe`,
66    /// `ConnectionAborted`, …). Routine for clients such as Home Assistant.
67    #[error("connection reset: {0:?}")]
68    Reset(io::ErrorKind),
69
70    /// The peer sent a `DisconnectRequest` and we completed the graceful
71    /// disconnect handshake.
72    #[error("disconnect requested by peer")]
73    Requested,
74
75    /// The write half of the connection stopped while the read half was still
76    /// processing, so a reply could no longer be delivered.
77    #[error("write side closed")]
78    WriteClosed,
79}
80
81/// A frame arrived but could not be decoded into a [`crate::parser::ProtoMessage`].
82#[derive(Debug, Error)]
83#[non_exhaustive]
84pub enum FrameError {
85    /// A well-formed frame carrying a message type this build does not know.
86    ///
87    /// Safe to skip: the connection stays healthy. This is the forward-compat
88    /// path for message types added by newer ESPHome versions.
89    #[error("unknown message type {0}")]
90    UnknownMessageType(u16),
91
92    /// A recognized message type whose protobuf body failed to decode.
93    #[error("failed to decode message type {message_type}")]
94    Decode {
95        /// The message type byte that failed to decode.
96        message_type: u16,
97    },
98
99    /// AEAD decryption of an encrypted frame failed (tampered ciphertext or a
100    /// desynchronized cipher).
101    #[error("frame decryption failed")]
102    Decrypt,
103
104    /// The frame itself was malformed (bad length varint, wrong preamble byte,
105    /// truncated payload, oversized frame, …).
106    #[error("malformed frame: {0}")]
107    Malformed(String),
108}
109
110/// Failures while establishing a connection or performing the encryption
111/// handshake. All of these are returned before the read/write loops start.
112#[derive(Debug, Error)]
113#[non_exhaustive]
114pub enum HandshakeError {
115    /// No bytes were received on a freshly accepted connection (for example, a
116    /// port probe that connects and immediately closes).
117    #[error("no data received")]
118    NoData,
119
120    /// The first byte was neither `0` (plaintext) nor `1` (encrypted).
121    #[error("invalid marker byte {0}")]
122    InvalidMarker(u8),
123
124    /// The peer and this device disagree on transport encryption: one side
125    /// offered plaintext while the other requires (or forbids) an encrypted
126    /// connection. The contained string describes which side mismatched.
127    #[error("encryption protocol mismatch: {0}")]
128    EncryptionProtocolMismatch(&'static str),
129
130    /// The peer sent a handshake frame that is too short to be valid.
131    #[error("malformed handshake frame")]
132    MalformedFrame,
133
134    /// The noise handshake failed its MAC check (wrong encryption key).
135    #[error("Handshake MAC failure")]
136    MacFailure,
137
138    /// The noise handshake produced a cryptographic error while building our
139    /// response. Unexpected; indicates a protocol or state problem rather than a
140    /// peer disconnect.
141    #[error("handshake crypto failure: {0}")]
142    Crypto(String),
143
144    /// The peer went away mid-handshake, before it could complete.
145    #[error("peer disconnected during handshake")]
146    Aborted,
147}