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}