Skip to content

Commit 190a11f

Browse files
phip1611rbradford
authored andcommitted
ch-remote: also pretty-print remote server errors
Remote server errors are transferred as raw HTTP body. This way, we lose the nested structured error information. This is an attempt to retrieve the errors from the HTTP response and to align the output with the normal error output. For example, this produces the following chain of errors. Note that everything after level 0 was retrieved from the HTTP server response: ``` Error: ch-remote exited with the following chain of errors: 0: http client error 1: Server responded with InternalServerError 2: Error from API 3: The disk could not be added to the VM 4: Failed to validate config 5: Identifier disk1 is not unique Debug Info: HttpApiClient(ServerResponse(InternalServerError, Some("Error from API<br>The disk could not be added to the VM<br>Failed to validate config<br>Identifier disk1 is not unique"))) ``` In case the JSON can't be parsed properly, ch-remote will print: ``` Error: ch-remote exited with the following chain of errors: 0: http client error X: Can't get remote's error messages from JSON response: EOF while parsing a value at line 1 column 0: body='' Debug Info: HttpApiClient(ServerResponse(InternalServerError, Some(""))) ``` Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de> On-behalf-of: SAP philipp.schuster@sap.com
1 parent 6ea1327 commit 190a11f

5 files changed

Lines changed: 108 additions & 15 deletions

File tree

api_client/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ pub enum Error {
2121
MissingProtocol,
2222
#[error("Error parsing HTTP Content-Length field")]
2323
ContentLengthParsing(#[source] std::num::ParseIntError),
24-
#[error("Server responded with an error: {0:?}: {1:?}")]
25-
ServerResponse(StatusCode, Option<String>),
24+
#[error("Server responded with error {0:?}: {1:?}")]
25+
ServerResponse(
26+
StatusCode,
27+
// TODO: Move `api` module from `vmm` to dedicated crate and use a common type definition
28+
Option<
29+
String, /* Untyped: Currently Vec<String> of error messages from top to root cause */
30+
>,
31+
),
2632
}
2733

2834
#[derive(Clone, Copy, Debug)]

src/bin/ch-remote.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1132,7 +1132,70 @@ fn main() {
11321132
};
11331133

11341134
if let Err(top_error) = target_api.do_command(&matches) {
1135-
cloud_hypervisor::cli_print_error_chain(&top_error, "ch-remote");
1135+
// Helper to join strings with a newline.
1136+
fn join_strs(mut acc: String, next: String) -> String {
1137+
if !acc.is_empty() {
1138+
acc.push('\n');
1139+
}
1140+
acc.push_str(&next);
1141+
acc
1142+
}
1143+
1144+
// This function helps to modify the Display representation of remote
1145+
// API failures so that it aligns with the regular output of error
1146+
// messages. As we transfer a deep/rich chain of errors as String via
1147+
// the HTTP API, the nested error chain is lost. We retrieve it from
1148+
// the error response.
1149+
//
1150+
// In case the repose itself is broken, the error is printed directly
1151+
// by using the `X` level.
1152+
fn server_api_error_display_modifier(
1153+
level: usize,
1154+
indention: usize,
1155+
error: &(dyn std::error::Error + 'static),
1156+
) -> Option<String> {
1157+
if let Some(api_client::Error::ServerResponse(status_code, body)) =
1158+
error.downcast_ref::<api_client::Error>()
1159+
{
1160+
let body = body.as_ref().map(|body| body.as_str()).unwrap_or("");
1161+
1162+
// Retrieve the list of error messages back.
1163+
let lines: Vec<&str> = match serde_json::from_str(body) {
1164+
Ok(json) => json,
1165+
Err(e) => {
1166+
return Some(format!(
1167+
"{idention}X: Can't get remote's error messages from JSON response: {e}: body='{body}'",
1168+
idention = " ".repeat(indention)
1169+
));
1170+
}
1171+
};
1172+
1173+
let error_status = format!("Server responded with {status_code:?}");
1174+
// Prepend the error status line to the lines iter.
1175+
let lines = std::iter::once(error_status.as_str()).chain(lines);
1176+
let error_msg_multiline = lines
1177+
.enumerate()
1178+
.map(|(index, error_msg)| (index + level, error_msg))
1179+
.map(|(level, error_msg)| {
1180+
format!(
1181+
"{idention}{level}: {error_msg}",
1182+
idention = " ".repeat(indention)
1183+
)
1184+
})
1185+
.fold(String::new(), join_strs);
1186+
1187+
return Some(error_msg_multiline);
1188+
}
1189+
1190+
None
1191+
}
1192+
1193+
let top_error: &dyn std::error::Error = &top_error;
1194+
cloud_hypervisor::cli_print_error_chain(
1195+
top_error,
1196+
"ch-remote",
1197+
server_api_error_display_modifier,
1198+
);
11361199
process::exit(1)
11371200
};
11381201
}

src/lib.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@ use std::error::Error;
77
/// Prints a chain of errors to the user in a consistent manner.
88
/// The user will see a clear chain of errors, followed by debug output
99
/// for opening issues.
10-
pub fn cli_print_error_chain(top_error: &dyn Error, component: &str) {
10+
pub fn cli_print_error_chain<'a>(
11+
top_error: &'a (dyn Error + 'static),
12+
component: &str,
13+
// Function optionally returning the display representation of an error.
14+
display_modifier: impl Fn(
15+
/* level */ usize,
16+
/*indention */ usize,
17+
&'a (dyn Error + 'static),
18+
) -> Option<String>,
19+
) {
1120
eprint!("Error: {component} exited with the following ");
1221
if top_error.source().is_none() {
1322
eprintln!("error:");
@@ -21,7 +30,12 @@ pub fn cli_print_error_chain(top_error: &dyn Error, component: &str) {
2130
})
2231
.enumerate()
2332
.for_each(|(level, error)| {
24-
eprintln!(" {level}: {error}",);
33+
// Special case: handling of HTTP Server responses in ch-remote
34+
if let Some(message) = display_modifier(level, 2, error) {
35+
eprintln!("{message}");
36+
} else {
37+
eprintln!(" {level}: {error}");
38+
}
2539
});
2640
}
2741

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ fn main() {
884884
0
885885
}
886886
Err(top_error) => {
887-
cloud_hypervisor::cli_print_error_chain(&top_error, "Cloud Hypervisor");
887+
cloud_hypervisor::cli_print_error_chain(&top_error, "Cloud Hypervisor", |_, _, _| None);
888888
1
889889
}
890890
};

vmm/src/api/http/mod.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55

66
use std::collections::BTreeMap;
7+
use std::error::Error;
78
use std::fs::File;
89
use std::os::unix::io::{IntoRawFd, RawFd};
910
use std::os::unix::net::UnixListener;
@@ -69,20 +70,29 @@ pub enum HttpError {
6970

7071
const HTTP_ROOT: &str = "/api/v1";
7172

72-
/// Creates the error response's body meant to be sent back to an API client.
73+
/// Creates the error response's JSON body meant to be sent back to an API client.
74+
///
7375
/// The error message contained in the response is supposed to be user-facing,
7476
/// thus insightful and helpful while balancing technical accuracy and
7577
/// simplicity.
7678
pub fn error_response(error: HttpError, status: StatusCode) -> Response {
7779
let mut response = Response::new(Version::Http11, status);
78-
// We must use debug output here without `#`, as it is currently the only
79-
// feasible option to get all relevant error details to the receiver,
80-
// i.e., ch-remote, in a balanced form. The Display impl is not guaranteed
81-
// to hold all relevant or helpful data.
82-
//
83-
// TODO: We might print a nice error chain here as well and send it to the
84-
// remote, similar to the normal error reporting?
85-
response.set_body(Body::new(format!("{error:?}")));
80+
81+
let error: &dyn Error = &error;
82+
// Write the Display::display() output all errors (from top to root).
83+
let error_messages = std::iter::successors(Some(error), |sub_error| {
84+
// Dereference necessary to mitigate rustc compiler bug.
85+
// See <https://github.com/rust-lang/rust/issues/141673>
86+
(*sub_error).source()
87+
})
88+
.map(|error| format!("{error}"))
89+
.collect::<Vec<_>>();
90+
91+
// TODO: Move `api` module from `vmm` to dedicated crate and use a common type definition
92+
let json = serde_json::to_string(&error_messages).unwrap();
93+
94+
let body = Body::new(json);
95+
response.set_body(body);
8696

8797
response
8898
}

0 commit comments

Comments
 (0)