Skip to content

Commit 3280f36

Browse files
Merge pull request #92 from mkuratczyk/parse-error-reason
Parse error/reason from failed API calls
2 parents 185257a + 3bff326 commit 3280f36

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

src/api/client.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
#![allow(clippy::result_large_err)]
1515

1616
use crate::commons::RetrySettings;
17-
use crate::error::Error::{ClientErrorResponse, NotFound, ServerErrorResponse};
17+
use crate::error::{
18+
Error::{ClientErrorResponse, NotFound, ServerErrorResponse},
19+
ErrorDetails,
20+
};
1821
use backtrace::Backtrace;
1922
use log::trace;
2023
use reqwest::{Client as HttpClient, StatusCode, header::HeaderMap};
@@ -636,9 +639,11 @@ where
636639
// so we copy the key parts into the error first
637640
let body = response.text().await?;
638641
trace!("HTTP API response: {} from {}: {}", status, url, body);
642+
let error_details = ErrorDetails::from_json(&body);
639643
return Err(ClientErrorResponse {
640644
url: Some(url),
641645
body: Some(body),
646+
error_details,
642647
headers: Some(headers),
643648
status_code: status,
644649
backtrace: Backtrace::new(),
@@ -657,9 +662,11 @@ where
657662
// so we copy the key parts into the error first
658663
let body = response.text().await?;
659664
trace!("HTTP API response: {} from {}: {}", status, url, body);
665+
let error_details = ErrorDetails::from_json(&body);
660666
return Err(ServerErrorResponse {
661667
url: Some(url),
662668
body: Some(body),
669+
error_details,
663670
headers: Some(headers),
664671
status_code: status,
665672
backtrace: Backtrace::new(),

src/blocking_api/client.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
#![allow(clippy::result_large_err)]
1515

1616
use crate::commons::RetrySettings;
17-
use crate::error::Error::{ClientErrorResponse, NotFound, ServerErrorResponse};
17+
use crate::error::{
18+
Error::{ClientErrorResponse, NotFound, ServerErrorResponse},
19+
ErrorDetails,
20+
};
1821
use backtrace::Backtrace;
1922
use log::trace;
2023
use reqwest::{StatusCode, blocking::Client as HttpClient, header::HeaderMap};
@@ -620,9 +623,11 @@ where
620623
// so we copy the key parts into the error first
621624
let body = response.text()?;
622625
trace!("Client error response: {} from {}: {}", status, url, body);
626+
let error_details = ErrorDetails::from_json(&body);
623627
return Err(ClientErrorResponse {
624628
url: Some(url),
625629
body: Some(body),
630+
error_details,
626631
headers: Some(headers),
627632
status_code: status,
628633
backtrace: Backtrace::new(),
@@ -641,9 +646,11 @@ where
641646
// so we copy the key parts into the error first
642647
let body = response.text()?;
643648
trace!("Server error response: {} from {}: {}", status, url, body);
649+
let error_details = ErrorDetails::from_json(&body);
644650
return Err(ServerErrorResponse {
645651
url: Some(url),
646652
body: Some(body),
653+
error_details,
647654
headers: Some(headers),
648655
status_code: status,
649656
backtrace: Backtrace::new(),

src/error.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use reqwest::{
2020
StatusCode, Url,
2121
header::{HeaderMap, InvalidHeaderValue},
2222
};
23+
use serde::Deserialize;
2324

2425
#[derive(Error, Debug)]
2526
pub enum ConversionError {
@@ -31,13 +32,34 @@ pub enum ConversionError {
3132
ParsingError { message: String },
3233
}
3334

35+
/// The API returns JSON with "error" and "reason" fields in error responses.
36+
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
37+
pub struct ErrorDetails {
38+
/// Generic error type, e.g., "bad_request"
39+
pub error: Option<String>,
40+
/// Detailed reason for the error
41+
pub reason: Option<String>,
42+
}
43+
44+
impl ErrorDetails {
45+
pub fn from_json(body: &str) -> Option<Self> {
46+
serde_json::from_str(body).ok()
47+
}
48+
49+
/// `reason` (typically more detailed) over `error` (generic).
50+
pub fn reason(&self) -> Option<&str> {
51+
self.reason.as_deref().or(self.error.as_deref())
52+
}
53+
}
54+
3455
#[derive(Error, Debug)]
3556
pub enum Error<U, S, E, BT> {
3657
#[error("API responded with a client error: status code of {status_code}")]
3758
ClientErrorResponse {
3859
url: Option<U>,
3960
status_code: S,
4061
body: Option<String>,
62+
error_details: Option<ErrorDetails>,
4163
headers: Option<HeaderMap>,
4264
backtrace: BT,
4365
},
@@ -46,6 +68,7 @@ pub enum Error<U, S, E, BT> {
4668
url: Option<U>,
4769
status_code: S,
4870
body: Option<String>,
71+
error_details: Option<ErrorDetails>,
4972
headers: Option<HeaderMap>,
5073
backtrace: BT,
5174
},
@@ -96,6 +119,7 @@ impl From<reqwest::Error> for HttpClientError {
96119
url: req_err.url().cloned(),
97120
status_code,
98121
body: None,
122+
error_details: None,
99123
headers: None,
100124
backtrace: Backtrace::new(),
101125
};
@@ -106,6 +130,7 @@ impl From<reqwest::Error> for HttpClientError {
106130
url: req_err.url().cloned(),
107131
status_code,
108132
body: None,
133+
error_details: None,
109134
headers: None,
110135
backtrace: Backtrace::new(),
111136
};

tests/blocking_policy_tests.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,94 @@ fn test_an_operator_policy(rc: &Client<&str, &str, &str>, policy: &PolicyParams)
276276
let policies = rc.list_operator_policies().unwrap();
277277
assert!(!policies.iter().any(|p| p.name == policy.name));
278278
}
279+
280+
#[test]
281+
fn test_blocking_policy_validation_error() {
282+
let endpoint = endpoint();
283+
let rc = Client::new(endpoint.as_str(), USERNAME, PASSWORD);
284+
285+
let vh_params = VirtualHostParams::named("test_policy_validation_error");
286+
let _ = rc.delete_vhost(vh_params.name, false);
287+
let result1 = rc.create_vhost(&vh_params);
288+
assert!(result1.is_ok());
289+
290+
// Create a policy with an invalid/unknown property in the definition
291+
let mut map = Map::<String, Value>::new();
292+
map.insert("foo".to_owned(), json!("bar"));
293+
map.insert("invalid-setting".to_owned(), json!(12345));
294+
let invalid_definition = map.clone();
295+
296+
let invalid_policy = PolicyParams {
297+
vhost: vh_params.name,
298+
name: "invalid_policy",
299+
pattern: "^qq$",
300+
apply_to: PolicyTarget::Queues,
301+
priority: 1,
302+
definition: invalid_definition,
303+
};
304+
305+
// Attempting to declare this policy should fail with a validation error
306+
let result = rc.declare_policy(&invalid_policy);
307+
assert!(
308+
result.is_err(),
309+
"Expected policy declaration to fail with validation error"
310+
);
311+
312+
// Extract the error and verify it contains structured error details
313+
if let Err(err) = result {
314+
match err {
315+
rabbitmq_http_client::error::Error::ClientErrorResponse {
316+
status_code,
317+
error_details,
318+
..
319+
} => {
320+
// Should be a 400 Bad Request
321+
assert_eq!(status_code, reqwest::StatusCode::BAD_REQUEST);
322+
323+
// Should have parsed error details
324+
assert!(
325+
error_details.is_some(),
326+
"Expected error_details to be parsed"
327+
);
328+
329+
let details = error_details.unwrap();
330+
331+
// Should have an error type
332+
assert!(
333+
details.error.is_some(),
334+
"Expected error field to be present"
335+
);
336+
assert_eq!(details.error.as_deref(), Some("bad_request"));
337+
338+
// Should have a detailed reason
339+
assert!(
340+
details.reason.is_some(),
341+
"Expected reason field to be present"
342+
);
343+
let reason = details.reason.as_ref().unwrap();
344+
345+
// The reason should mention validation failure
346+
assert!(
347+
reason.contains("Validation failed"),
348+
"Expected reason to contain 'Validation failed', got: {}",
349+
reason
350+
);
351+
352+
// The reason should mention unrecognized properties
353+
assert!(
354+
reason.contains("not recognised") || reason.contains("not recognized"),
355+
"Expected reason to mention unrecognized properties, got: {}",
356+
reason
357+
);
358+
359+
// Verify reason() returns the reason (more detailed message)
360+
let detailed_msg = details.reason();
361+
assert!(detailed_msg.is_some());
362+
assert_eq!(detailed_msg, details.reason.as_deref());
363+
}
364+
_ => panic!("Expected ClientErrorResponse, got: {:?}", err),
365+
}
366+
}
367+
368+
let _ = rc.delete_vhost(vh_params.name, false);
369+
}

0 commit comments

Comments
 (0)