Skip to main content

netget/utils/
wire_failure.rs

1//! What a network peer may be told when netget itself fails.
2//!
3//! **Rule: an internal error string never reaches the wire.** Not the LLM backend's error,
4//! not an `anyhow` context chain, not a serde or codec message. Those name the model, the
5//! backend URL, the request body, file paths and library internals; the peer is a stranger
6//! and none of it is theirs to read. It goes to the log and the TUI status stream, which is
7//! where an operator looks.
8//!
9//! This is not a hypothetical. Every protocol that answers its peer on backend failure used
10//! to interpolate the error into the reply, so a plain `telnet` session got
11//! `[netget] cannot answer right now: ✗ LLM failed to generate valid response after retries.`
12//! — netget's own retry machinery, verbatim, on a stranger's terminal.
13//!
14//! A peer still needs an answer: silence leaves it blocked until its own timeout, which is
15//! the failure mode CLAUDE.md calls out as worse than refusing. So the protocols keep
16//! answering — they just answer with a *category*, not a diagnosis.
17//!
18//! ## Why `&'static str`
19//!
20//! [`WireFailure::text`] returns `&'static str` on purpose. A function that took an error and
21//! returned `String` would be one `format!` away from leaking again, and that is exactly how
22//! the original bug spread: each protocol copied its neighbour. A `&'static str` cannot carry
23//! anything derived from the error, so the guarantee is in the type rather than in reviewer
24//! attention.
25//!
26//! The error is still passed in — but only to *classify* it, never to render it.
27
28/// How an internal failure is described to a network peer.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum WireFailure {
31    /// The backend refused because it is saturated. Transient: a retry may well succeed, and
32    /// protocols that have a distinct "try again" code (503, RESP `LOADING`, MySQL 1205,
33    /// gRPC `UNAVAILABLE`) should use it so a client backs off instead of recording a
34    /// permanent fault.
35    Overloaded,
36    /// Anything else — the backend erred, timed out, or answered with something unusable.
37    /// Not retryable as far as the peer is concerned.
38    Unavailable,
39}
40
41impl WireFailure {
42    /// Classify an error without rendering it.
43    pub fn classify(err: &anyhow::Error) -> Self {
44        if crate::llm::is_overload_error(err) {
45            Self::Overloaded
46        } else {
47            Self::Unavailable
48        }
49    }
50
51    /// The peer-visible text.
52    ///
53    /// Safe as free text in any protocol's single-line reply: ASCII, no CR or LF (which would
54    /// forge a second response in the line-oriented protocols), no leading `.` (which would
55    /// terminate a POP3/NNTP multiline block), and short enough for a fixed-width field.
56    pub fn text(self) -> &'static str {
57        match self {
58            Self::Overloaded => "backend at capacity, retry later",
59            Self::Unavailable => "request could not be processed",
60        }
61    }
62
63    /// The same text with netget's name in front, for protocols whose free-text field is
64    /// otherwise attributed to the origin server.
65    ///
66    /// Naming the software is not debug information — it is what an HTTP `Server:` header
67    /// does — and it tells an operator poking at their own server which process answered.
68    pub fn prefixed_text(self) -> &'static str {
69        match self {
70            Self::Overloaded => "netget: backend at capacity, retry later",
71            Self::Unavailable => "netget: request could not be processed",
72        }
73    }
74
75    pub fn is_overloaded(self) -> bool {
76        matches!(self, Self::Overloaded)
77    }
78}
79
80/// Shorthand for [`WireFailure::classify`] + [`WireFailure::text`].
81pub fn wire_failure_text(err: &anyhow::Error) -> &'static str {
82    WireFailure::classify(err).text()
83}
84
85/// Shorthand for [`WireFailure::classify`] + [`WireFailure::prefixed_text`].
86pub fn prefixed_wire_failure_text(err: &anyhow::Error) -> &'static str {
87    WireFailure::classify(err).prefixed_text()
88}