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
//! cargo run --example errors (no API key, no network needed)
//! Every failure the SDK reports and how to tell them apart. The four it can provoke offline
//! are provoked here; the rest are matched in `report` so the shape of each one is visible.
use std::time::Duration;
use typesafe::{ApiErrorKind, Client, Error, Noul, Questions, RetryPolicy};
#[tokio::main]
async fn main() {
// 1. Config — the client could not be built. Raised by `build`, before anything is sent.
let err = Client::builder()
.api_key("not-a-real-key")
.base_url("ftp://api.typesafe.ai")
.build()
.expect_err("ftp is not an http(s) URL");
report("bad base_url", err);
// A client that points at a closed port, so nothing here leaves the machine.
let client = Client::builder()
.api_key("not-a-real-key")
.base_url("http://127.0.0.1:9")
.timeout(Duration::from_millis(500))
.retry(RetryPolicy::none())
.build()
.expect("a valid configuration");
// 2. InvalidRequest — rejected locally: no questions, empty criteria, unencodable state.
let err = client
.system_one("anything", Questions::new())
.await
.expect_err("a request with no questions is refused");
report("no questions", err);
// 3. Connection — the request never produced a response.
let err = client
.system_one("anything", Questions::from([("ok", Noul::new("Fine?"))]))
.await
.expect_err("nothing is listening on port 9");
report("unreachable server", err);
// 4. ReplayMiss — replaying recorded responses, and this request was never recorded.
let replaying = Client::builder()
.replay(std::env::temp_dir().join("typesafe-errors-example-empty"))
.build()
.expect("replaying needs no key");
let err = replaying
.system_one("anything", Questions::from([("ok", Noul::new("Fine?"))]))
.await
.expect_err("nothing was recorded there");
report("not recorded", err);
}
/// One place that knows what to do with each kind of failure.
fn report(label: &str, err: Error) {
print!("{label}: ");
match err {
// Programmer error: fix the configuration and restart.
Error::Config(message) => println!("configuration — {message}"),
// Programmer error too: the request was never sent, so retrying it changes nothing.
Error::InvalidRequest(message) => println!("invalid request — {message}"),
// The server answered, unhappily. `kind` classifies the status.
Error::Api(api) => {
print!("HTTP {} ({:?}) — {}", api.status, api.kind, api.message);
if let Some(id) = api.request_id() {
print!(" [request {id}]");
}
println!();
match api.kind {
// Already retried by the default policy; `retry_after` is what the server asked for.
ApiErrorKind::RateLimit => println!(" back off for {:?}", api.retry_after()),
// A bad key is not worth retrying.
ApiErrorKind::Authentication => println!(" check TYPESAFE_API_KEY"),
// The body was rejected; `api.body` holds the field-by-field detail.
ApiErrorKind::UnprocessableEntity => println!(" body: {:?}", api.body),
_ => {}
}
}
// No response: DNS, connect, TLS, reset, body read. The transport error is the source.
Error::Connection(source) => println!("no response — {source}"),
// An attempt ran out of time. Retried by default; this is what is left after that.
Error::Timeout(after) => println!("timed out after {after:?}"),
// A 2xx whose body does not match the schema. `field_path` says exactly where.
Error::ResponseValidation(bad) => {
println!("unusable response at {} — {}", bad.field_path, bad.detail);
}
// A test replaying recorded responses asked something new. Record it, then replay again.
Error::ReplayMiss { path, .. } => println!("not recorded — no {}", path.display()),
// `Error` is `#[non_exhaustive]`: later versions may add variants.
other => println!("{other}"),
}
}