Skip to content

Commit 7eef72e

Browse files
Ben StriegelKevinWMatthews
Ben Striegel
authored andcommitted
refactor(ilp-cli): error handling
1 parent 0abd8fb commit 7eef72e

File tree

4 files changed

+67
-88
lines changed

4 files changed

+67
-88
lines changed

Cargo.lock

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ilp-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repository = "https://github.com/interledger-rs/interledger-rs"
99

1010
[dependencies]
1111
clap = { version = "2.33.0", default-features = false }
12-
failure = { version = "0.1.6", default-features = false }
12+
thiserror = { version = "1.0.4", default-features = false }
1313
http = { version = "0.1.18", default-features = false }
1414
reqwest = { version = "0.9.21", default-features = false, features = ["default-tls"] }
1515
serde = { version = "1.0.101", default-features = false }

crates/ilp-cli/src/interpreter.rs

Lines changed: 33 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,32 @@
11
use clap::ArgMatches;
2-
use failure::Fail;
32
use http;
43
use reqwest::{self, Client, Response};
54
use std::{borrow::Cow, collections::HashMap};
65
use tungstenite::{connect, handshake::client::Request};
76
use url::Url;
87

9-
#[derive(Debug, Fail)]
8+
#[derive(Debug, thiserror::Error)]
109
pub enum Error {
11-
#[fail(display = "Usage error")]
10+
// Custom errors
11+
#[error("Usage error")]
1212
UsageErr(&'static str),
13-
#[fail(display = "Error sending request: {}", _0)]
14-
ClientErr(reqwest::Error),
15-
#[fail(display = "Error receiving response: {}", _0)]
16-
ResponseErr(String),
17-
#[fail(display = "Error parsing URL protocol: {}", _0)]
18-
ProtocolErr(&'static str),
19-
#[fail(display = "Error altering URL scheme")]
20-
SchemeErr(),
21-
#[fail(display = "Error parsing URL: {}", _0)]
22-
UrlErr(url::ParseError),
23-
#[fail(display = "Error connecting to WebSocket host: {}", _0)]
24-
WebsocketErr(tungstenite::error::Error),
25-
}
26-
27-
impl From<url::ParseError> for Error {
28-
fn from(error: url::ParseError) -> Self {
29-
Error::UrlErr(error)
30-
}
31-
}
32-
33-
impl From<tungstenite::error::Error> for Error {
34-
fn from(error: tungstenite::error::Error) -> Self {
35-
Error::WebsocketErr(error)
36-
}
13+
#[error("Invalid protocol in URL: {0}")]
14+
ProtocolErr(String),
15+
// Foreign errors
16+
#[error("Error sending HTTP request: {0}")]
17+
ClientErr(#[from] reqwest::Error),
18+
#[error("Error altering URL scheme")]
19+
SchemeErr(()), // TODO: should be part of UrlError, see https://github.com/servo/rust-url/issues/299
20+
#[error("Error parsing URL: {0}")]
21+
UrlErr(#[from] url::ParseError),
22+
#[error("WebSocket error: {0}")]
23+
WebsocketErr(#[from] tungstenite::error::Error),
3724
}
3825

3926
pub fn run(matches: &ArgMatches) -> Result<Response, Error> {
4027
let client = NodeClient {
4128
client: Client::new(),
42-
// `--node` has a a default value, so will never be None
43-
url: matches.value_of("node_url").unwrap(),
29+
url: matches.value_of("node_url").unwrap(), // infallible unwrap
4430
};
4531

4632
// Dispatch based on parsed input
@@ -92,7 +78,7 @@ impl NodeClient<'_> {
9278
// GET /accounts/:username/balance
9379
fn get_account_balance(&self, matches: &ArgMatches) -> Result<Response, Error> {
9480
let (auth, mut args) = extract_args(matches);
95-
let user = args.remove("username").unwrap();
81+
let user = args.remove("username").unwrap(); // infallible unwrap
9682
self.client
9783
.get(&format!("{}/accounts/{}/balance", self.url, user))
9884
.bearer_auth(auth)
@@ -143,12 +129,13 @@ impl NodeClient<'_> {
143129
let scheme = match url.scheme() {
144130
"http" => Ok("ws"),
145131
"https" => Ok("wss"),
146-
_ => Err(Error::ProtocolErr("Unexpected protocol")),
132+
s => Err(Error::ProtocolErr(format!(
133+
"{} (only HTTP and HTTPS are supported)",
134+
s
135+
))),
147136
}?;
148137

149-
if url.set_scheme(scheme).is_err() {
150-
return Err(Error::SchemeErr());
151-
};
138+
url.set_scheme(scheme).map_err(Error::SchemeErr)?;
152139

153140
let mut request: Request = url.into();
154141
request.add_header(
@@ -158,9 +145,7 @@ impl NodeClient<'_> {
158145

159146
let (mut socket, _) = connect(request)?;
160147
loop {
161-
let msg = socket
162-
.read_message()
163-
.expect("Could not receive WebSocket message");
148+
let msg = socket.read_message()?;
164149
println!("{}", msg);
165150
}
166151
}
@@ -188,7 +173,7 @@ impl NodeClient<'_> {
188173
// PUT /accounts/:username/settings
189174
fn put_account_settings(&self, matches: &ArgMatches) -> Result<Response, Error> {
190175
let (auth, mut args) = extract_args(matches);
191-
let user = args.remove("username").unwrap();
176+
let user = args.remove("username").unwrap(); // infallible unwrap
192177
self.client
193178
.put(&format!("{}/accounts/{}/settings", self.url, user))
194179
.bearer_auth(auth)
@@ -200,7 +185,7 @@ impl NodeClient<'_> {
200185
// POST /accounts/:username/payments
201186
fn post_account_payments(&self, matches: &ArgMatches) -> Result<Response, Error> {
202187
let (auth, mut args) = extract_args(matches);
203-
let user = args.remove("sender_username").unwrap();
188+
let user = args.remove("sender_username").unwrap(); // infallible unwrap
204189
self.client
205190
.post(&format!("{}/accounts/{}/payments", self.url, user))
206191
.bearer_auth(&format!("{}:{}", user, auth))
@@ -276,6 +261,7 @@ impl NodeClient<'_> {
276261
.send()
277262
.map_err(Error::ClientErr)
278263
}
264+
279265
/*
280266
{"http_endpoint": "https://rs3.xpring.dev/ilp", // ilp_over_http_url
281267
"passkey": "b0i3q9tbvfgek", // ilp_over_http_outgoing_token = username:passkey
@@ -287,7 +273,6 @@ impl NodeClient<'_> {
287273
"payment_pointer": "$rs3.xpring.dev/accounts/user_g31tuju4/spsp"}
288274
routing_relation Parent
289275
*/
290-
291276
fn xpring_account(&self, matches: &ArgMatches) -> Result<Response, Error> {
292277
let (auth, cli_args) = extract_args(matches);
293278
// Note the Xpring API expects the asset code in lowercase
@@ -296,31 +281,17 @@ impl NodeClient<'_> {
296281
.client
297282
.get(&format!("https://xpring.io/api/accounts/{}", asset))
298283
.send()
299-
.map_err(|err| {
300-
Error::ResponseErr(format!(
301-
"Error requesting credentials from Xpring Testnet Signup API: {:?}",
302-
err
303-
))
304-
})?
284+
.map_err(Error::ClientErr)?
305285
.json()
306-
.map_err(|err| {
307-
Error::ResponseErr(format!(
308-
"Got unexpected response from Xpring Testnet Signup API: {:?}",
309-
err
310-
))
311-
})?;
286+
.map_err(Error::ClientErr)?;
312287
let mut args = HashMap::new();
313-
let token = format!(
314-
"{}:{}",
315-
foreign_args.username.clone(),
316-
foreign_args.passkey.clone()
317-
);
318-
args.insert("ilp_over_http_url", foreign_args.http_endpoint.clone());
288+
let token = format!("{}:{}", foreign_args.username, foreign_args.passkey);
289+
args.insert("ilp_over_http_url", foreign_args.http_endpoint);
319290
args.insert("ilp_over_http_outgoing_token", token.clone());
320-
args.insert("ilp_over_btp_url", foreign_args.btp_endpoint.clone());
291+
args.insert("ilp_over_btp_url", foreign_args.btp_endpoint);
321292
args.insert("ilp_over_btp_outgoing_token", token.clone());
322293
args.insert("asset_scale", foreign_args.asset_scale.to_string());
323-
args.insert("asset_code", foreign_args.asset_code.clone());
294+
args.insert("asset_code", foreign_args.asset_code);
324295
args.insert("username", format!("xpring_{}", asset));
325296
args.insert("routing_relation", String::from("Parent")); // TODO: weird behavior when deleting and re-inserting accounts with this
326297
// TODO should we set different parameters?
@@ -335,9 +306,9 @@ impl NodeClient<'_> {
335306
.send();
336307

337308
if matches.is_present("return_testnet_credential") {
338-
result.expect("Error creating account for testnet node on our local node");
309+
result?;
339310
Ok(Response::from(
340-
http::Response::builder().body(token).unwrap(),
311+
http::Response::builder().body(token).unwrap(), // infallible unwrap
341312
))
342313
} else {
343314
result.map_err(Error::ClientErr)

crates/ilp-cli/src/main.rs

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ pub fn main() {
2020
app.get_matches_from(s.split(' '));
2121
}
2222
Err(e) => {
23-
eprintln!("ILP CLI error: {}", e);
23+
eprintln!("ilp-cli error: {}", e);
2424
exit(1);
2525
}
2626
Ok(mut response) => match response.text() {
2727
Err(e) => {
28-
eprintln!("ILP CLI error: Failed to parse HTTP response: {}", e);
28+
eprintln!("ilp-cli error: Failed to parse HTTP response: {}", e);
2929
exit(1);
3030
}
3131
Ok(body) => {
@@ -35,7 +35,7 @@ pub fn main() {
3535
}
3636
} else {
3737
eprintln!(
38-
"ILP CLI error: Unexpected response from server: {}: {}",
38+
"ilp-cli error: Unexpected response from server: {}: {}",
3939
response.status(),
4040
body,
4141
);
@@ -55,9 +55,6 @@ pub fn main() {
5555
// run the interpreter in order to detect panics.
5656
// Conveniently this section also serves as a reference for example invocations.
5757
mod interface_tests {
58-
use crate::interpreter;
59-
use crate::parser;
60-
6158
#[test]
6259
fn ilp_cli() {
6360
should_parse(&[
@@ -103,18 +100,6 @@ mod interface_tests {
103100
]);
104101
}
105102

106-
#[test]
107-
fn accounts_incoming_payments_invalid_protocol() {
108-
should_parse(&[
109-
"ilp-cli --node tftp://localhost:7770 accounts incoming-payments alice --auth foo",
110-
]);
111-
}
112-
113-
#[test]
114-
fn accounts_incoming_payments_invalid_host() {
115-
should_parse(&["ilp-cli --node http://:7770 accounts incoming-payments alice --auth foo"]);
116-
}
117-
118103
#[test]
119104
fn accounts_info() {
120105
should_parse(&[
@@ -211,16 +196,19 @@ mod interface_tests {
211196
}
212197

213198
fn should_parse(examples: &[&str]) {
199+
use crate::interpreter::{run, Error};
200+
use crate::parser;
201+
214202
let mut app = parser::build();
215203
for example in examples {
216204
let parser_result = app.get_matches_from_safe_borrow(example.split(' '));
217205
match parser_result {
218-
Err(e) => panic!("Failure while parsing command `{}`: {}", example, e),
219-
Ok(matches) => {
220-
// Any unanticipated errors at this stage will result in a panic from
221-
// within the interpreter, so no need to manually check the result.
222-
let _interpreter_result = interpreter::run(&matches);
223-
}
206+
Err(e) => panic!("Failed to parse command `{}`: {}", example, e),
207+
Ok(matches) => match run(&matches) {
208+
// Because these are interface tests, not integration tests, network errors are expected
209+
Ok(_) | Err(Error::ClientErr(_)) | Err(Error::WebsocketErr(_)) => (),
210+
Err(e) => panic!("Unexpected interpreter failure: {}", e),
211+
},
224212
}
225213
}
226214
}

0 commit comments

Comments
 (0)