11use anyhow:: anyhow;
22use async_trait:: async_trait;
3+ use reqwest:: header:: { self , HeaderValue } ;
34use reqwest:: { self , Client , Proxy , RequestBuilder , Response , StatusCode } ;
45use slog:: { debug, Logger } ;
56use std:: { io, sync:: Arc , time:: Duration } ;
67use thiserror:: Error ;
78
89use mithril_common:: {
910 api_version:: APIVersionProvider ,
10- entities:: { Epoch , ProtocolMessage , SignedEntityType , Signer , SingleSignatures } ,
11+ entities:: {
12+ ClientError , Epoch , ProtocolMessage , ServerError , SignedEntityType , Signer ,
13+ SingleSignatures ,
14+ } ,
1115 logging:: LoggerExtensions ,
1216 messages:: {
1317 AggregatorFeaturesMessage , EpochSettingsMessage , TryFromMessageAdapter , TryToMessageAdapter ,
@@ -21,6 +25,8 @@ use crate::message_adapters::{
2125} ;
2226use crate :: services:: SignaturePublisher ;
2327
28+ const JSON_CONTENT_TYPE : HeaderValue = HeaderValue :: from_static ( "application/json" ) ;
29+
2430/// Error structure for the Aggregator Client.
2531#[ derive( Error , Debug ) ]
2632pub enum AggregatorClientError {
@@ -36,6 +42,10 @@ pub enum AggregatorClientError {
3642 #[ error( "remote server unreachable" ) ]
3743 RemoteServerUnreachable ( #[ source] StdError ) ,
3844
45+ /// Unhandled status code
46+ #[ error( "unhandled status code: {0}, response text: {1}" ) ]
47+ UnhandledStatusCode ( StatusCode , String ) ,
48+
3949 /// Could not parse response.
4050 #[ error( "json parsing failed" ) ]
4151 JsonParseFailed ( #[ source] StdError ) ,
@@ -69,6 +79,65 @@ impl AggregatorClientError {
6979 }
7080}
7181
82+ impl AggregatorClientError {
83+ /// Create an `AggregatorClientError` from a response.
84+ ///
85+ /// This method is meant to be used after handling domain-specific cases leaving only
86+ /// 4xx or 5xx status codes.
87+ /// Otherwise, it will return an `UnhandledStatusCode` error.
88+ pub async fn from_response ( response : Response ) -> Self {
89+ let error_code = response. status ( ) ;
90+
91+ if error_code. is_client_error ( ) {
92+ let root_cause = Self :: get_root_cause ( response) . await ;
93+ Self :: RemoteServerLogical ( anyhow ! ( root_cause) )
94+ } else if error_code. is_server_error ( ) {
95+ let root_cause = Self :: get_root_cause ( response) . await ;
96+ Self :: RemoteServerTechnical ( anyhow ! ( root_cause) )
97+ } else {
98+ let response_text = response. text ( ) . await . unwrap_or_default ( ) ;
99+ Self :: UnhandledStatusCode ( error_code, response_text)
100+ }
101+ }
102+
103+ async fn get_root_cause ( response : Response ) -> String {
104+ let error_code = response. status ( ) ;
105+ let canonical_reason = error_code. canonical_reason ( ) . unwrap_or_default ( ) ;
106+ let is_json = response
107+ . headers ( )
108+ . get ( header:: CONTENT_TYPE )
109+ . is_some_and ( |ct| JSON_CONTENT_TYPE == ct) ;
110+
111+ if is_json {
112+ let json_value: serde_json:: Value = response. json ( ) . await . unwrap_or_default ( ) ;
113+
114+ if let Ok ( client_error) = serde_json:: from_value :: < ClientError > ( json_value. clone ( ) ) {
115+ format ! (
116+ "{}: {}: {}" ,
117+ canonical_reason. to_lowercase( ) ,
118+ client_error. label,
119+ client_error. message
120+ )
121+ } else if let Ok ( server_error) =
122+ serde_json:: from_value :: < ServerError > ( json_value. clone ( ) )
123+ {
124+ format ! (
125+ "{}: {}" ,
126+ canonical_reason. to_lowercase( ) ,
127+ server_error. message
128+ )
129+ } else if json_value. is_null ( ) {
130+ canonical_reason. to_lowercase ( ) . to_string ( )
131+ } else {
132+ format ! ( "{}: {}" , canonical_reason. to_lowercase( ) , json_value)
133+ }
134+ } else {
135+ let response_text = response. text ( ) . await . unwrap_or_default ( ) ;
136+ format ! ( "{}: {}" , canonical_reason. to_lowercase( ) , response_text)
137+ }
138+ }
139+ }
140+
72141/// Trait for mocking and testing a `AggregatorClient`
73142#[ cfg_attr( test, mockall:: automock) ]
74143#[ async_trait]
@@ -216,10 +285,7 @@ impl AggregatorClient for AggregatorHTTPClient {
216285 Err ( err) => Err ( AggregatorClientError :: JsonParseFailed ( anyhow ! ( err) ) ) ,
217286 } ,
218287 StatusCode :: PRECONDITION_FAILED => Err ( self . handle_api_error ( & response) ) ,
219- _ => Err ( AggregatorClientError :: RemoteServerTechnical ( anyhow ! (
220- "{}" ,
221- response. text( ) . await . unwrap_or_default( )
222- ) ) ) ,
288+ _ => Err ( AggregatorClientError :: from_response ( response) . await ) ,
223289 } ,
224290 Err ( err) => Err ( AggregatorClientError :: RemoteServerUnreachable ( anyhow ! ( err) ) ) ,
225291 }
@@ -245,13 +311,7 @@ impl AggregatorClient for AggregatorHTTPClient {
245311 Ok ( response) => match response. status ( ) {
246312 StatusCode :: CREATED => Ok ( ( ) ) ,
247313 StatusCode :: PRECONDITION_FAILED => Err ( self . handle_api_error ( & response) ) ,
248- StatusCode :: BAD_REQUEST => Err ( AggregatorClientError :: RemoteServerLogical (
249- anyhow ! ( "bad request: {}" , response. text( ) . await . unwrap_or_default( ) ) ,
250- ) ) ,
251- _ => Err ( AggregatorClientError :: RemoteServerTechnical ( anyhow ! (
252- "{}" ,
253- response. text( ) . await . unwrap_or_default( )
254- ) ) ) ,
314+ _ => Err ( AggregatorClientError :: from_response ( response) . await ) ,
255315 } ,
256316 Err ( err) => Err ( AggregatorClientError :: RemoteServerUnreachable ( anyhow ! ( err) ) ) ,
257317 }
@@ -285,16 +345,10 @@ impl AggregatorClient for AggregatorHTTPClient {
285345 Ok ( ( ) )
286346 }
287347 StatusCode :: PRECONDITION_FAILED => Err ( self . handle_api_error ( & response) ) ,
288- StatusCode :: BAD_REQUEST => Err ( AggregatorClientError :: RemoteServerLogical (
289- anyhow ! ( "bad request: {}" , response. text( ) . await . unwrap_or_default( ) ) ,
290- ) ) ,
291348 StatusCode :: CONFLICT => Err ( AggregatorClientError :: RemoteServerLogical ( anyhow ! (
292349 "already registered single signatures"
293350 ) ) ) ,
294- _ => Err ( AggregatorClientError :: RemoteServerTechnical ( anyhow ! (
295- "{}" ,
296- response. text( ) . await . unwrap_or_default( )
297- ) ) ) ,
351+ _ => Err ( AggregatorClientError :: from_response ( response) . await ) ,
298352 } ,
299353 Err ( err) => Err ( AggregatorClientError :: RemoteServerUnreachable ( anyhow ! ( err) ) ) ,
300354 }
@@ -317,10 +371,7 @@ impl AggregatorClient for AggregatorHTTPClient {
317371 . await
318372 . map_err ( |e| AggregatorClientError :: JsonParseFailed ( anyhow ! ( e) ) ) ?) ,
319373 StatusCode :: PRECONDITION_FAILED => Err ( self . handle_api_error ( & response) ) ,
320- _ => Err ( AggregatorClientError :: RemoteServerTechnical ( anyhow ! (
321- "{}" ,
322- response. text( ) . await . unwrap_or_default( )
323- ) ) ) ,
374+ _ => Err ( AggregatorClientError :: from_response ( response) . await ) ,
324375 } ,
325376 Err ( err) => Err ( AggregatorClientError :: RemoteServerUnreachable ( anyhow ! ( err) ) ) ,
326377 }
@@ -426,10 +477,11 @@ pub(crate) mod dumb {
426477
427478#[ cfg( test) ]
428479mod tests {
480+ use http:: response:: Builder as HttpResponseBuilder ;
429481 use httpmock:: prelude:: * ;
430482 use serde_json:: json;
431483
432- use mithril_common:: entities:: { ClientError , Epoch } ;
484+ use mithril_common:: entities:: Epoch ;
433485 use mithril_common:: era:: { EraChecker , SupportedEra } ;
434486 use mithril_common:: messages:: TryFromMessageAdapter ;
435487 use mithril_common:: test_utils:: fake_data;
@@ -494,6 +546,34 @@ mod tests {
494546 } ) ;
495547 }
496548
549+ fn build_text_response < T : Into < String > > ( status_code : StatusCode , body : T ) -> Response {
550+ HttpResponseBuilder :: new ( )
551+ . status ( status_code)
552+ . body ( body. into ( ) )
553+ . unwrap ( )
554+ . into ( )
555+ }
556+
557+ fn build_json_response < T : serde:: Serialize > ( status_code : StatusCode , body : & T ) -> Response {
558+ HttpResponseBuilder :: new ( )
559+ . status ( status_code)
560+ . header ( header:: CONTENT_TYPE , JSON_CONTENT_TYPE )
561+ . body ( serde_json:: to_string ( & body) . unwrap ( ) )
562+ . unwrap ( )
563+ . into ( )
564+ }
565+
566+ macro_rules! assert_error_text_contains {
567+ ( $error: expr, $expect_contains: expr) => {
568+ let error = & $error;
569+ assert!(
570+ error. contains( $expect_contains) ,
571+ "Expected error message to contain '{}'\n got '{error:?}'" ,
572+ $expect_contains,
573+ ) ;
574+ } ;
575+ }
576+
497577 #[ tokio:: test]
498578 async fn test_aggregator_features_ok_200 ( ) {
499579 let ( server, client) = setup_server_and_client ( ) ;
@@ -1001,4 +1081,113 @@ mod tests {
10011081 "unexpected error type: {error:?}"
10021082 ) ;
10031083 }
1084+
1085+ #[ tokio:: test]
1086+ async fn test_4xx_errors_are_handled_as_remote_server_logical ( ) {
1087+ let response = build_text_response ( StatusCode :: BAD_REQUEST , "error text" ) ;
1088+ let handled_error = AggregatorClientError :: from_response ( response) . await ;
1089+
1090+ assert ! (
1091+ matches!(
1092+ handled_error,
1093+ AggregatorClientError :: RemoteServerLogical ( ..)
1094+ ) ,
1095+ "Expected error to be RemoteServerLogical\n got '{handled_error:?}'" ,
1096+ ) ;
1097+ }
1098+
1099+ #[ tokio:: test]
1100+ async fn test_5xx_errors_are_handled_as_remote_server_technical ( ) {
1101+ let response = build_text_response ( StatusCode :: INTERNAL_SERVER_ERROR , "error text" ) ;
1102+ let handled_error = AggregatorClientError :: from_response ( response) . await ;
1103+
1104+ assert ! (
1105+ matches!(
1106+ handled_error,
1107+ AggregatorClientError :: RemoteServerTechnical ( ..)
1108+ ) ,
1109+ "Expected error to be RemoteServerLogical\n got '{handled_error:?}'" ,
1110+ ) ;
1111+ }
1112+
1113+ #[ tokio:: test]
1114+ async fn test_non_4xx_or_5xx_errors_are_handled_as_unhandled_status_code_and_contains_response_text (
1115+ ) {
1116+ let response = build_text_response ( StatusCode :: OK , "ok text" ) ;
1117+ let handled_error = AggregatorClientError :: from_response ( response) . await ;
1118+
1119+ assert ! (
1120+ matches!(
1121+ handled_error,
1122+ AggregatorClientError :: UnhandledStatusCode ( ..) if format!( "{handled_error:?}" ) . contains( "ok text" )
1123+ ) ,
1124+ "Expected error to be UnhandledStatusCode with 'ok text' in error text\n got '{handled_error:?}'" ,
1125+ ) ;
1126+ }
1127+
1128+ #[ tokio:: test]
1129+ async fn test_root_cause_of_non_json_response_contains_response_plain_text ( ) {
1130+ let error_text = "An error occurred; please try again later." ;
1131+ let response = build_text_response ( StatusCode :: EXPECTATION_FAILED , error_text) ;
1132+
1133+ assert_error_text_contains ! (
1134+ AggregatorClientError :: get_root_cause( response) . await ,
1135+ "expectation failed: An error occurred; please try again later."
1136+ ) ;
1137+ }
1138+
1139+ #[ tokio:: test]
1140+ async fn test_root_cause_of_json_formatted_client_error_response_contains_error_label_and_message (
1141+ ) {
1142+ let client_error = ClientError :: new ( "label" , "message" ) ;
1143+ let response = build_json_response ( StatusCode :: BAD_REQUEST , & client_error) ;
1144+
1145+ assert_error_text_contains ! (
1146+ AggregatorClientError :: get_root_cause( response) . await ,
1147+ "bad request: label: message"
1148+ ) ;
1149+ }
1150+
1151+ #[ tokio:: test]
1152+ async fn test_root_cause_of_json_formatted_server_error_response_contains_error_label_and_message (
1153+ ) {
1154+ let server_error = ServerError :: new ( "message" ) ;
1155+ let response = build_json_response ( StatusCode :: BAD_REQUEST , & server_error) ;
1156+
1157+ assert_error_text_contains ! (
1158+ AggregatorClientError :: get_root_cause( response) . await ,
1159+ "bad request: message"
1160+ ) ;
1161+ }
1162+
1163+ #[ tokio:: test]
1164+ async fn test_root_cause_of_unknown_formatted_json_response_contains_json_key_value_pairs ( ) {
1165+ let response = build_json_response (
1166+ StatusCode :: INTERNAL_SERVER_ERROR ,
1167+ & json ! ( { "second" : "unknown" , "first" : "foreign" } ) ,
1168+ ) ;
1169+
1170+ assert_error_text_contains ! (
1171+ AggregatorClientError :: get_root_cause( response) . await ,
1172+ r#"internal server error: {"first":"foreign","second":"unknown"}"#
1173+ ) ;
1174+ }
1175+
1176+ #[ tokio:: test]
1177+ async fn test_root_cause_with_invalid_json_response_still_contains_response_status_name ( ) {
1178+ let response = HttpResponseBuilder :: new ( )
1179+ . status ( StatusCode :: BAD_REQUEST )
1180+ . header ( header:: CONTENT_TYPE , JSON_CONTENT_TYPE )
1181+ . body ( r#"{"invalid":"unexpected dot", "key": "value".}"# )
1182+ . unwrap ( )
1183+ . into ( ) ;
1184+
1185+ let root_cause = AggregatorClientError :: get_root_cause ( response) . await ;
1186+
1187+ assert_error_text_contains ! ( root_cause, "bad request" ) ;
1188+ assert ! (
1189+ !root_cause. contains( "bad request: " ) ,
1190+ "Expected error message should not contain additional information \n got '{root_cause:?}'"
1191+ ) ;
1192+ }
10041193}
0 commit comments