1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
//! The typed failure of one `execute` call: what went wrong last, and how
//! many attempts were made before giving up.
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
/// The server answered with a non-2xx status.
Status(u16),
/// No response at all: connect, timeout, TLS, body read.
Transport(String),
/// The circuit breaker was open and the policy said fail fast.
BreakerOpen,
/// The auth refresher returned an error.
Auth(String),
/// The client's semaphore was closed. Reserved for an explicit client
/// shutdown; nothing closes the semaphore today.
Closed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientError {
/// The failure of the last attempt.
pub kind: ErrorKind,
/// Attempts made, including the first one.
pub attempts: usize,
/// The start of the last non-2xx response body (up to 2 KiB, UTF-8
/// lossy), for callers that surface the server's own message — an outbox
/// entry's `last_error`, a lens log line. `None` when there was no
/// response body to read, and for transport, breaker and auth failures.
/// Not part of [`Display`](fmt::Display).
pub body: Option<String>,
}
impl ClientError {
pub(crate) fn new(kind: ErrorKind, attempts: usize) -> Self {
Self {
kind,
attempts,
body: None,
}
}
pub(crate) fn with_body(mut self, body: Option<String>) -> Self {
self.body = body;
self
}
/// Whether no retry, now or later, could change this answer: a `4xx`
/// other than `408` / `429`, an auth refresher that failed, or a closed
/// client. Everything else (`5xx`, transport, an open breaker) is the
/// transient kind a later call may get past, so callers log it quietly.
pub fn is_final(&self) -> bool {
match self.kind {
ErrorKind::Status(s) => !super::policy::is_retryable_status(s),
ErrorKind::Auth(_) | ErrorKind::Closed => true,
ErrorKind::Transport(_) | ErrorKind::BreakerOpen => false,
}
}
/// The HTTP status of the last attempt, when there was a response.
pub fn status(&self) -> Option<u16> {
match self.kind {
ErrorKind::Status(s) => Some(s),
_ => None,
}
}
/// A stable lowercase name for the kind, for logs and tests.
pub fn kind_name(&self) -> &'static str {
match self.kind {
ErrorKind::Status(_) => "status",
ErrorKind::Transport(_) => "transport",
ErrorKind::BreakerOpen => "breaker_open",
ErrorKind::Auth(_) => "auth",
ErrorKind::Closed => "closed",
}
}
}
impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let plural = if self.attempts == 1 {
"attempt"
} else {
"attempts"
};
match &self.kind {
ErrorKind::Status(s) => write!(f, "HTTP {s} after {} {plural}", self.attempts),
ErrorKind::Transport(e) => write!(f, "{e} after {} {plural}", self.attempts),
ErrorKind::BreakerOpen => write!(f, "circuit breaker open"),
ErrorKind::Auth(e) => write!(f, "auth refresh failed: {e}"),
ErrorKind::Closed => write!(f, "client closed"),
}
}
}
impl std::error::Error for ClientError {}