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
//! The one error type every public function returns.
use std::time::Duration;
/// Everything that can go wrong, from the wire to the policy.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// `401`: missing or invalid API key.
#[error("unauthorized: missing or invalid TypeSafe API key")]
Auth,
/// `422`: the request body failed validation. `detail` is the verbatim response body.
#[error("invalid request: {detail}")]
Invalid {
/// The response body, JSON envelope included; it names the offending field.
detail: String,
},
/// `429` after every retry was used.
#[error("rate limited (retry-after: {retry_after:?})")]
RateLimited {
/// The last `retry-after` the API sent, if any.
retry_after: Option<Duration>,
},
/// `529` after every retry was used.
#[error("TypeSafe is overloaded (retry-after: {retry_after:?})")]
Overloaded {
/// The last `retry-after` the API sent, if any.
retry_after: Option<Duration>,
},
/// Connection, TLS, timeout, or body-read failure. A failure to connect — refused, reset,
/// or a TLS handshake — is retried inside the same budget as a throttle. A timeout of any
/// phase and a body failure are not: the client sets one deadline over the whole attempt,
/// so a connect-phase timeout cannot be told apart from a read timeout, and retrying
/// either would multiply the wall time that deadline promises.
#[error("transport failure: {0}")]
Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
/// A status the API contract does not define.
#[error("unexpected status {status}: {body}")]
UnexpectedStatus {
/// HTTP status code.
status: u16,
/// Response body, verbatim.
body: String,
},
/// The response violated the contract: undecodable body, answer kind mismatch, unknown
/// option or level, probability outside `0..=1`, missing answer for a question.
#[error("protocol violation: {detail}")]
Protocol {
/// What was violated.
detail: String,
},
/// The policy labelled the answer unsure and no fallback was given.
#[error("unsure answer for question {question}: {value} against threshold {threshold}")]
Unsure {
/// Question id (`q0`, `q1`, …) in encode order; the same id the wire and the span use.
question: String,
/// The probability (noul) or confidence (choice, score) that was judged.
value: f64,
/// The boundary the value fell short of: the nearer band edge for noul,
/// `min_confidence` for choice and score.
threshold: f64,
},
/// Bad configuration: thresholds outside `0..=1`, `no_below > yes_above`, missing key,
/// empty batch, unserialisable state, empty or duplicate rubric.
#[error("configuration error: {detail}")]
Config {
/// What was wrong.
detail: String,
},
}
impl Error {
/// A stable, low-cardinality name for the variant: `auth`, `invalid`, `rate_limited`,
/// `overloaded`, `transport`, `unexpected_status`, `protocol`, `unsure` or `config`.
///
/// This is the value of the `error.type` attribute on a failed span, so it is safe to
/// group metrics by.
pub fn kind(&self) -> &'static str {
match self {
Error::Auth => "auth",
Error::Invalid { .. } => "invalid",
Error::RateLimited { .. } => "rate_limited",
Error::Overloaded { .. } => "overloaded",
Error::Transport(_) => "transport",
Error::UnexpectedStatus { .. } => "unexpected_status",
Error::Protocol { .. } => "protocol",
Error::Unsure { .. } => "unsure",
Error::Config { .. } => "config",
}
}
}