Skip to main content

lager/
error.rs

1//! Crate-wide error type.
2
3use std::fmt;
4
5/// Convenience alias used by every fallible API in this crate.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// All the ways a Lager box interaction can fail.
9#[derive(Debug)]
10#[non_exhaustive]
11pub enum Error {
12    /// The box could not be reached at all (DNS, TCP connect, transport
13    /// failure). Check network/Tailscale and that the box is online.
14    Connection(String),
15    /// The HTTP request timed out client-side. For long-running actions
16    /// (energy integration windows, `wait_for_level`) the crate widens the
17    /// timeout automatically, so hitting this usually means the box stalled.
18    Timeout(String),
19    /// The box accepted the request but reported failure. Carries the HTTP
20    /// status and the box's `error` message. Note the box can report failure
21    /// with HTTP 200 (e.g. a cross-role instrument conflict); this variant
22    /// covers that too.
23    Box {
24        /// HTTP status code of the response (200 is possible: the box
25        /// reports some conflicts as `success: false` with HTTP 200).
26        status: u16,
27        /// Human-readable error message from the box.
28        message: String,
29    },
30    /// Box-side `dfu-util` ran and exited non-zero (HTTP 502 from
31    /// `POST /usb/dfu`). Carries both captured streams in full, and
32    /// `Display` quotes both: dfu-util reports a device failure on stdout
33    /// for a raw download and on stderr for DfuSe, and its DFU suffix
34    /// warnings on stderr.
35    DfuFailed {
36        /// HTTP status code of the response (502).
37        status: u16,
38        /// dfu-util's exit code.
39        exit_code: Option<i64>,
40        /// dfu-util's captured stdout.
41        stdout: String,
42        /// dfu-util's captured stderr.
43        stderr: String,
44    },
45    /// A debug `flash` ran but programmed nothing (box >= 0.51.0). The box
46    /// answers HTTP 200 with `programmed: false` for this, so before this
47    /// variant the crate reported success.
48    FlashFailed {
49        /// The box's verdict: the line showing nothing was programmed.
50        error: String,
51        /// The backend's output.
52        output: Vec<String>,
53    },
54    /// The debug probe stayed busy with another operation (a flash, erase,
55    /// reset, ...) for longer than the box waits (HTTP 503 from the debug
56    /// service, box >= 0.51.0). Retry once the other operation is done.
57    ProbeBusy {
58        /// The box's message, naming the operation holding the probe.
59        message: String,
60    },
61    /// The net's safety settings refused a destructive debug operation
62    /// (flash or erase; HTTP 403 from the debug service). See
63    /// [`crate::SafetyLimits::allow_destructive`].
64    SafetyRefused {
65        /// The box's message.
66        message: String,
67    },
68    /// HTTP 501: this box image does not serve the endpoint. The box needs a
69    /// software update.
70    UnsupportedByBox {
71        /// Human-readable error message from the box.
72        message: String,
73    },
74    /// The box's response could not be parsed into the expected shape.
75    Decode(String),
76    /// The net type exists in the Lager Python API but its box endpoint is
77    /// not yet available on the `:9000` HTTP API, so this crate ships it as a
78    /// documented stub. No net type returns this since the scope stub became
79    /// a real net ([`crate::nets::scope`]) in 0.8.0; the variant stays so matches
80    /// on it keep compiling.
81    NotSupportedByBox {
82        /// Which net/feature was invoked (e.g. `"debug"`).
83        feature: &'static str,
84        /// What the box side would need to expose for this to work.
85        details: &'static str,
86    },
87    /// The box sits behind an authenticating gateway and the request was
88    /// denied (HTTP 401 with the `X-Gateway-Auth-Url` discovery header),
89    /// with no usable credential available. Sign in with the Lager CLI
90    /// (`lager login <auth_url>`) — this crate reuses the CLI's session —
91    /// or supply a token via `LagerBoxBuilder::bearer_token` /
92    /// `LAGER_GATEWAY_TOKEN`.
93    AuthRequired {
94        /// Hostname of the gated box.
95        box_host: String,
96        /// The auth server URL announced by the gateway.
97        auth_url: String,
98        /// What happened (no credential vs. rejected credential).
99        message: String,
100    },
101    /// Client-side configuration problem (bad host string, missing env var).
102    Config(String),
103    /// A streaming session (UART) reported an error, e.g. the net is in use
104    /// by another session or the device disappeared.
105    Stream(String),
106}
107
108impl fmt::Display for Error {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Error::Connection(msg) => write!(
112                f,
113                "cannot reach the Lager box: {msg}. Check network/Tailscale and that the box is online and updated"
114            ),
115            Error::Timeout(msg) => write!(f, "request to the Lager box timed out: {msg}"),
116            Error::Box { status, message } => {
117                write!(f, "box error (HTTP {status}): {message}")
118            }
119            Error::DfuFailed {
120                status,
121                exit_code,
122                stdout,
123                stderr,
124            } => {
125                write!(f, "box error (HTTP {status}): dfu-util ")?;
126                match exit_code {
127                    Some(code) => write!(f, "exited with code {code}")?,
128                    None => write!(f, "failed")?,
129                }
130                let (stderr, stdout) = (stderr.trim(), stdout.trim());
131                if stderr.is_empty() && stdout.is_empty() {
132                    return write!(f, ": no output");
133                }
134                if !stderr.is_empty() {
135                    write!(f, "\nstderr:\n{stderr}")?;
136                }
137                if !stdout.is_empty() {
138                    write!(f, "\nstdout:\n{stdout}")?;
139                }
140                Ok(())
141            }
142            Error::FlashFailed { error, output } => {
143                write!(f, "flash failed: {error}")?;
144                if !output.is_empty() {
145                    write!(f, "\noutput:\n{}", output.join("\n"))?;
146                }
147                Ok(())
148            }
149            Error::ProbeBusy { message } => write!(f, "debug probe busy: {message}"),
150            Error::SafetyRefused { message } => {
151                write!(f, "refused by the net's safety settings: {message}")
152            }
153            Error::UnsupportedByBox { message } => write!(
154                f,
155                "{message}. This box image does not support this endpoint; update the box"
156            ),
157            Error::Decode(msg) => write!(f, "could not decode box response: {msg}"),
158            Error::NotSupportedByBox { feature, details } => write!(
159                f,
160                "'{feature}' is not yet available over the box HTTP API: {details}"
161            ),
162            Error::AuthRequired { auth_url, message, .. } => write!(
163                f,
164                "{message}. Sign in with `lager login {auth_url}` (this crate reuses the \
165                 CLI's session), or set LAGER_GATEWAY_TOKEN / use \
166                 LagerBoxBuilder::bearer_token"
167            ),
168            Error::Config(msg) => write!(f, "configuration error: {msg}"),
169            Error::Stream(msg) => write!(f, "streaming session error: {msg}"),
170        }
171    }
172}
173
174impl std::error::Error for Error {}
175
176impl From<serde_json::Error> for Error {
177    fn from(e: serde_json::Error) -> Self {
178        Error::Decode(e.to_string())
179    }
180}