Skip to content

Commit 744ea0c

Browse files
committed
Make mithril-client bubble up aggregator error text
Before it would replace the whole text with "Unhandled error {status_code}", loosing potential details on the error sent by the aggregator.
1 parent 4b49084 commit 744ea0c

File tree

2 files changed

+131
-17
lines changed

2 files changed

+131
-17
lines changed

mithril-client/src/aggregator_client.rs

Lines changed: 111 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@
77
//!
88
//! An implementation using HTTP is available: [AggregatorHTTPClient].
99
10+
use std::sync::Arc;
11+
1012
use anyhow::{anyhow, Context};
1113
use async_recursion::async_recursion;
1214
use async_trait::async_trait;
1315
use reqwest::{Response, StatusCode, Url};
1416
use semver::Version;
1517
use slog::{debug, Logger};
16-
use std::sync::Arc;
1718
use thiserror::Error;
1819
use tokio::sync::RwLock;
1920

20-
#[cfg(test)]
21-
use mockall::automock;
22-
21+
use mithril_common::entities::{ClientError, ServerError};
2322
use mithril_common::MITHRIL_API_VERSION_HEADER;
2423

2524
use crate::{MithrilError, MithrilResult};
@@ -28,11 +27,11 @@ use crate::{MithrilError, MithrilResult};
2827
#[derive(Error, Debug)]
2928
pub enum AggregatorClientError {
3029
/// Error raised when querying the aggregator returned a 5XX error.
31-
#[error("remote server technical error")]
30+
#[error("Internal error of the Aggregator")]
3231
RemoteServerTechnical(#[source] MithrilError),
3332

3433
/// Error raised when querying the aggregator returned a 4XX error.
35-
#[error("remote server logical error")]
34+
#[error("Invalid request to the Aggregator")]
3635
RemoteServerLogical(#[source] MithrilError),
3736

3837
/// Error raised when the server API version mismatch the client API version.
@@ -258,11 +257,12 @@ impl AggregatorHTTPClient {
258257
Err(self.handle_api_error(&response).await)
259258
}
260259
StatusCode::NOT_FOUND => Err(AggregatorClientError::RemoteServerLogical(anyhow!(
261-
"Url='{url} not found"
262-
))),
263-
status_code => Err(AggregatorClientError::RemoteServerTechnical(anyhow!(
264-
"Unhandled error {status_code}"
260+
"Url='{url}' not found"
265261
))),
262+
status_code if status_code.is_client_error() => {
263+
Err(Self::remote_logical_error(response).await)
264+
}
265+
_ => Err(Self::remote_technical_error(response).await),
266266
}
267267
}
268268

@@ -303,9 +303,10 @@ impl AggregatorHTTPClient {
303303
StatusCode::NOT_FOUND => Err(AggregatorClientError::RemoteServerLogical(anyhow!(
304304
"Url='{url} not found"
305305
))),
306-
status_code => Err(AggregatorClientError::RemoteServerTechnical(anyhow!(
307-
"Unhandled error {status_code}"
308-
))),
306+
status_code if status_code.is_client_error() => {
307+
Err(Self::remote_logical_error(response).await)
308+
}
309+
_ => Err(Self::remote_technical_error(response).await),
309310
}
310311
}
311312

@@ -336,9 +337,32 @@ impl AggregatorHTTPClient {
336337
})
337338
.map_err(AggregatorClientError::SubsystemError)
338339
}
340+
341+
async fn remote_logical_error(response: Response) -> AggregatorClientError {
342+
let status_code = response.status();
343+
let client_error = response
344+
.json::<ClientError>()
345+
.await
346+
.unwrap_or(ClientError::new(
347+
format!("Unhandled error {status_code}"),
348+
"",
349+
));
350+
351+
AggregatorClientError::RemoteServerLogical(anyhow!("{client_error}"))
352+
}
353+
354+
async fn remote_technical_error(response: Response) -> AggregatorClientError {
355+
let status_code = response.status();
356+
let server_error = response
357+
.json::<ServerError>()
358+
.await
359+
.unwrap_or(ServerError::new(format!("Unhandled error {status_code}")));
360+
361+
AggregatorClientError::RemoteServerTechnical(anyhow!("{server_error}"))
362+
}
339363
}
340364

341-
#[cfg_attr(test, automock)]
365+
#[cfg_attr(test, mockall::automock)]
342366
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
343367
#[cfg_attr(not(target_family = "wasm"), async_trait)]
344368
impl AggregatorClient for AggregatorHTTPClient {
@@ -377,8 +401,30 @@ impl AggregatorClient for AggregatorHTTPClient {
377401

378402
#[cfg(test)]
379403
mod tests {
404+
use httpmock::MockServer;
405+
406+
use mithril_common::api_version::APIVersionProvider;
407+
use mithril_common::entities::{ClientError, ServerError};
408+
380409
use super::*;
381410

411+
macro_rules! assert_error_eq {
412+
($left:expr, $right:expr) => {
413+
assert_eq!(format!("{:?}", &$left), format!("{:?}", &$right),);
414+
};
415+
}
416+
417+
fn setup_server_and_client() -> (MockServer, AggregatorHTTPClient) {
418+
let server = MockServer::start();
419+
let client = AggregatorHTTPClient::new(
420+
Url::parse(&server.url("")).unwrap(),
421+
APIVersionProvider::compute_all_versions_sorted().unwrap(),
422+
crate::test_utils::test_logger(),
423+
)
424+
.expect("building aggregator http client should not fail");
425+
(server, client)
426+
}
427+
382428
#[test]
383429
fn always_append_trailing_slash_at_build() {
384430
for (expected, url) in [
@@ -482,4 +528,55 @@ mod tests {
482528
);
483529
}
484530
}
531+
532+
#[tokio::test]
533+
async fn test_client_handle_4xx_errors() {
534+
let client_error = ClientError::new("label", "message");
535+
536+
let (aggregator, client) = setup_server_and_client();
537+
aggregator.mock(|_when, then| {
538+
then.status(StatusCode::IM_A_TEAPOT.as_u16())
539+
.json_body_obj(&client_error);
540+
});
541+
542+
let expected_error = AggregatorClientError::RemoteServerLogical(anyhow!("{client_error}"));
543+
544+
let get_content_error = client
545+
.get_content(AggregatorRequest::ListCertificates)
546+
.await
547+
.unwrap_err();
548+
assert_error_eq!(get_content_error, expected_error);
549+
550+
let post_content_error = client
551+
.post_content(AggregatorRequest::ListCertificates)
552+
.await
553+
.unwrap_err();
554+
assert_error_eq!(post_content_error, expected_error);
555+
}
556+
557+
#[tokio::test]
558+
async fn test_client_handle_5xx_errors() {
559+
let server_error = ServerError::new("message");
560+
561+
let (aggregator, client) = setup_server_and_client();
562+
aggregator.mock(|_when, then| {
563+
then.status(StatusCode::INTERNAL_SERVER_ERROR.as_u16())
564+
.json_body_obj(&server_error);
565+
});
566+
567+
let expected_error =
568+
AggregatorClientError::RemoteServerTechnical(anyhow!("{server_error}"));
569+
570+
let get_content_error = client
571+
.get_content(AggregatorRequest::ListCertificates)
572+
.await
573+
.unwrap_err();
574+
assert_error_eq!(get_content_error, expected_error);
575+
576+
let post_content_error = client
577+
.post_content(AggregatorRequest::ListCertificates)
578+
.await
579+
.unwrap_err();
580+
assert_error_eq!(post_content_error, expected_error);
581+
}
485582
}

mithril-common/src/entities/http_server_error.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use crate::StdError;
1+
use std::fmt::Display;
2+
23
use serde::{Deserialize, Serialize};
34

5+
use crate::StdError;
6+
47
/// Representation of a Server Error raised by a http server
58
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
69
pub struct ServerError {
@@ -10,8 +13,16 @@ pub struct ServerError {
1013

1114
impl ServerError {
1215
/// InternalServerError factory
13-
pub fn new(message: String) -> ServerError {
14-
ServerError { message }
16+
pub fn new<M: Into<String>>(message: M) -> ServerError {
17+
ServerError {
18+
message: message.into(),
19+
}
20+
}
21+
}
22+
23+
impl Display for ServerError {
24+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25+
write!(f, "{}", self.message)
1526
}
1627
}
1728

@@ -52,3 +63,9 @@ impl ClientError {
5263
}
5364
}
5465
}
66+
67+
impl Display for ClientError {
68+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69+
write!(f, "{}: {}", self.label, self.message)
70+
}
71+
}

0 commit comments

Comments
 (0)