Many imorovments
This commit is contained in:
@@ -37,6 +37,20 @@ pub enum RemoteError {
|
||||
#[error("network error: {0}")]
|
||||
Network(String),
|
||||
|
||||
/// The TLS handshake was refused: an untrusted issuer, an expired
|
||||
/// certificate, a hostname mismatch.
|
||||
///
|
||||
/// **Distinct from [`Network`](Self::Network) on purpose.** The server is
|
||||
/// there and answering — the connection is refused on trust grounds, which
|
||||
/// no amount of waiting repairs. Folding it into `Network` puts the app
|
||||
/// into offline mode and tells the user their connection is down, sending
|
||||
/// them to inspect a network that is working perfectly (observed
|
||||
/// 2026-08-12: a Let's Encrypt chain anchored at ISRG Root YE, absent from
|
||||
/// the compiled-in root store, reported as "you are offline" on a server
|
||||
/// answering in 19 ms).
|
||||
#[error("TLS error: {0}")]
|
||||
Tls(String),
|
||||
|
||||
#[error("unexpected server response: {status} {detail}")]
|
||||
Server { status: u16, detail: String },
|
||||
|
||||
@@ -52,6 +66,9 @@ impl RemoteError {
|
||||
pub fn is_transient(&self) -> bool {
|
||||
match self {
|
||||
RemoteError::Network(_) => true,
|
||||
// Not transient: a rejected certificate is rejected identically on
|
||||
// every retry. Retrying one buys nothing and hides the cause.
|
||||
RemoteError::Tls(_) => false,
|
||||
RemoteError::Server { status, .. } => {
|
||||
// 5xx and 429 are worth retrying; other 4xx are not.
|
||||
//
|
||||
@@ -80,6 +97,10 @@ impl RemoteError {
|
||||
/// is just failing, and a retry is the right response rather than a
|
||||
/// mode change. 429 and 423 likewise — those are the server working
|
||||
/// correctly under load.
|
||||
///
|
||||
/// [`Tls`](Self::Tls) is likewise not offline. The host resolved, the
|
||||
/// socket connected, and the peer answered; only trust failed. Reporting
|
||||
/// that as offline is what made a root-store gap look like a dead network.
|
||||
pub fn indicates_offline(&self) -> bool {
|
||||
matches!(self, RemoteError::Network(_))
|
||||
}
|
||||
@@ -89,6 +110,29 @@ impl RemoteError {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_tls_failure_is_not_offline() {
|
||||
// The regression this guards: a rejected certificate was reported as
|
||||
// `Network`, which put the whole app into offline mode and told the
|
||||
// user their connection was down — while the server answered in 19 ms.
|
||||
let e = RemoteError::Tls("invalid peer certificate: UnknownIssuer".into());
|
||||
assert!(
|
||||
!e.indicates_offline(),
|
||||
"trust failure is not unreachability"
|
||||
);
|
||||
assert!(
|
||||
!e.is_transient(),
|
||||
"a rejected certificate is rejected identically on every retry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_dead_connection_is_still_offline() {
|
||||
// The other side of the split: separating TLS out must not stop a
|
||||
// genuine transport failure from reaching offline mode.
|
||||
assert!(RemoteError::Network("dns error".into()).indicates_offline());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transient_errors_are_retryable() {
|
||||
assert!(RemoteError::Network("timeout".into()).is_transient());
|
||||
|
||||
Reference in New Issue
Block a user