use crate::transaction::broadcaster::BroadcastFailure;
pub(crate) fn classify_reqwest_err(e: &reqwest::Error) -> (&'static str, String) {
let code = if e.is_timeout() {
"NETWORK_TIMEOUT"
} else if e.is_connect() {
"NETWORK_CONNECT"
} else if e.is_request() {
"NETWORK_REQUEST"
} else {
"NETWORK_ERROR"
};
let desc = match std::error::Error::source(e) {
Some(s) => format!("network error ({code}): {e} (source: {s})"),
None => format!("network error ({code}): {e}"),
};
(code, desc)
}
pub(crate) const MAX_BODY_PREVIEW_CHARS: usize = 4096;
pub(super) async fn parse_broadcast_body(
response: reqwest::Response,
) -> Result<(u32, serde_json::Value), BroadcastFailure> {
let status = response.status().as_u16() as u32;
let bytes = response.bytes().await.map_err(|e| BroadcastFailure {
status,
code: "READ_ERROR".to_string(),
description: format!("failed to read response body: {e}"),
..Default::default()
})?;
match serde_json::from_slice::<serde_json::Value>(&bytes) {
Ok(v) => Ok((status, v)),
Err(e) => {
let preview: String = String::from_utf8_lossy(&bytes)
.chars()
.take(MAX_BODY_PREVIEW_CHARS)
.collect();
let is_success = (200..300).contains(&status);
let code = if is_success {
"NON_JSON_BODY".to_string()
} else {
status.to_string()
};
Err(BroadcastFailure {
status,
code,
description: format!("non-JSON body: ({e}): {preview}"),
..Default::default()
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[tokio::test]
async fn test_parse_broadcast_body_truncates_oversized_body() {
let mock_server = MockServer::start().await;
let oversized: String = "A".repeat(8192);
Mock::given(method("POST"))
.and(path("/echo"))
.respond_with(ResponseTemplate::new(502).set_body_string(oversized.clone()))
.mount(&mock_server)
.await;
let resp = reqwest::Client::new()
.post(format!("{}/echo", mock_server.uri()))
.send()
.await
.expect("send");
let err = parse_broadcast_body(resp).await.unwrap_err();
assert_eq!(err.status, 502);
const PREFIX_OVERHEAD_BUDGET: usize = 256;
let upper_bound = MAX_BODY_PREVIEW_CHARS + PREFIX_OVERHEAD_BUDGET;
let actual_chars = err.description.chars().count();
assert!(
actual_chars <= upper_bound,
"description exceeded MAX_BODY_PREVIEW_CHARS+overhead: \
{} chars (cap was {} + {} overhead = {})",
actual_chars,
MAX_BODY_PREVIEW_CHARS,
PREFIX_OVERHEAD_BUDGET,
upper_bound
);
assert!(
actual_chars >= MAX_BODY_PREVIEW_CHARS,
"description shorter than MAX_BODY_PREVIEW_CHARS: {} chars",
actual_chars
);
let a_count = err.description.chars().filter(|c| *c == 'A').count();
assert_eq!(
a_count, MAX_BODY_PREVIEW_CHARS,
"expected exactly MAX_BODY_PREVIEW_CHARS 'A's in preview \
after truncation, got {}",
a_count
);
}
}