Error api change- replaced io::error with string based format
What changed, and why it matters
This commit is a code-quality refactor in the Lightning Dev Kit's block-sync module. It replaces the use of Rust's generic input/output error type with more specific string-based error messages when parsing HTTP and RPC responses. It does not appear to fix a security vulnerability, change network behavior, or introduce a new attack path. The main risk is that some error classifications may have shifted slightly (for example, parse errors are now consistently treated as persistent rather than transient), which could affect retry behavior but is not directly exploitable.
Treat as a routine API refactor. Reviewers should verify that the new persistent classification for parse errors matches operational intent (i.e., malformed responses should not be retried indefinitely). No immediate security response is warranted.
Security signals we found
Error-type refactor across block-sync HTTP/REST/RPC parsing paths
Parse errors now explicitly classified as persistent in BlockSourceError conversions
Removal of std::io::Error delegation in error conversion chain
No changes to authentication, deserialization bounds checking, or network request handling
Evidence from the diff
The patch changes error types across lightning-block-sync from std::io::Error to string/Infallible/() based errors. HttpClientError gains a Parse(String) variant instead of Io(std::io::Error). Conversion traits (TryFrom/TryInto) in convert.rs, http.rs, rest.rs, and rpc.rs are updated accordingly. The From
Changed components
lightning-block-sync/src/convert.rslightning-block-sync/src/http.rslightning-block-sync/src/rest.rslightning-block-sync/src/rpc.rsInspect captured patch +141 / −161
diff --git a/lightning-block-sync/src/convert.rs b/lightning-block-sync/src/convert.rs
index 47c7586..48a80c8 100644
--- a/lightning-block-sync/src/convert.rs
+++ b/lightning-block-sync/src/convert.rs
@@ -13,15 +13,15 @@ use bitcoin::Transaction;
use serde_json;
use bitcoin::hashes::Hash;
-use std::convert::From;
+use std::convert::Infallible;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::io;
use std::str::FromStr;
impl TryInto<serde_json::Value> for JsonResponse {
- type Error = io::Error;
- fn try_into(self) -> Result<serde_json::Value, io::Error> {
+ type Error = Infallible;
+ fn try_into(self) -> Result<serde_json::Value, Infallible> {
Ok(self.0)
}
}
@@ -53,8 +53,10 @@ impl From<HttpClientError> for BlockSourceError {
BlockSourceError::persistent(HttpClientError::Http(http_err))
}
},
- // Delegate to existing From<io::Error> implementation
- HttpClientError::Io(io_err) => BlockSourceError::from(io_err),
+ // Parse errors are persistent (invalid data)
+ HttpClientError::Parse(msg) => {
+ BlockSourceError::persistent(HttpClientError::Parse(msg))
+ },
}
}
}
@@ -81,7 +83,9 @@ impl From<RpcClientError> for BlockSourceError {
)))
}
},
- HttpClientError::Io(io_err) => BlockSourceError::from(io_err),
+ HttpClientError::Parse(msg) => {
+ BlockSourceError::persistent(RpcClientError::Http(HttpClientError::Parse(msg)))
+ },
},
// RPC errors (e.g. "block not found") are transient
RpcClientError::Rpc(rpc_err) => {
@@ -97,49 +101,42 @@ impl From<RpcClientError> for BlockSourceError {
/// Parses binary data as a block.
impl TryInto<Block> for BinaryResponse {
- type Error = io::Error;
+ type Error = ();
- fn try_into(self) -> io::Result<Block> {
- match encode::deserialize(&self.0) {
- Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid block data")),
- Ok(block) => Ok(block),
- }
+ fn try_into(self) -> Result<Block, ()> {
+ encode::deserialize(&self.0).map_err(|_| ())
}
}
/// Parses binary data as a block hash.
impl TryInto<BlockHash> for BinaryResponse {
- type Error = io::Error;
+ type Error = ();
- fn try_into(self) -> io::Result<BlockHash> {
- BlockHash::from_slice(&self.0)
- .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad block hash length"))
+ fn try_into(self) -> Result<BlockHash, ()> {
+ BlockHash::from_slice(&self.0).map_err(|_| ())
}
}
/// Converts a JSON value into block header data. The JSON value may be an object representing a
/// block header or an array of such objects. In the latter case, the first object is converted.
impl TryInto<BlockHeaderData> for JsonResponse {
- type Error = io::Error;
+ type Error = &'static str;
- fn try_into(self) -> io::Result<BlockHeaderData> {
+ fn try_into(self) -> Result<BlockHeaderData, &'static str> {
let header = match self.0 {
serde_json::Value::Array(mut array) if !array.is_empty() => {
array.drain(..).next().unwrap()
},
serde_json::Value::Object(_) => self.0,
- _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected JSON type")),
+ _ => return Err("unexpected JSON type"),
};
if !header.is_object() {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON object"));
+ return Err("expected JSON object");
}
// Add an empty previousblockhash for the genesis block.
- match header.try_into() {
- Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid header data")),
- Ok(header) => Ok(header),
- }
+ header.try_into().map_err(|_| "invalid header data")
}
}
@@ -179,15 +176,15 @@ impl TryFrom<serde_json::Value> for BlockHeaderData {
/// Converts a JSON value into a block. Assumes the block is hex-encoded in a JSON string.
impl TryInto<Block> for JsonResponse {
- type Error = io::Error;
+ type Error = &'static str;
- fn try_into(self) -> io::Result<Block> {
+ fn try_into(self) -> Result<Block, &'static str> {
match self.0.as_str() {
- None => Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")),
+ None => Err("expected JSON string"),
Some(hex_data) => match Vec::<u8>::from_hex(hex_data) {
- Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")),
+ Err(_) => Err("invalid hex data"),
Ok(block_data) => match encode::deserialize(&block_data) {
- Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid block data")),
+ Err(_) => Err("invalid block data"),
Ok(block) => Ok(block),
},
},
@@ -197,35 +194,31 @@ impl TryInto<Block> for JsonResponse {
/// Converts a JSON value into the best block hash and optional height.
impl TryInto<(BlockHash, Option<u32>)> for JsonResponse {
- type Error = io::Error;
+ type Error = &'static str;
- fn try_into(self) -> io::Result<(BlockHash, Option<u32>)> {
+ fn try_into(self) -> Result<(BlockHash, Option<u32>), &'static str> {
if !self.0.is_object() {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON object"));
+ return Err("expected JSON object");
}
let hash = match &self.0["bestblockhash"] {
serde_json::Value::String(hex_data) => match BlockHash::from_str(&hex_data) {
- Err(_) => {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hex data"))
- },
+ Err(_) => return Err("invalid hex data"),
Ok(block_hash) => block_hash,
},
- _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")),
+ _ => return Err("expected JSON string"),
};
let height = match &self.0["blocks"] {
serde_json::Value::Null => None,
serde_json::Value::Number(height) => match height.as_u64() {
- None => return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid height")),
+ None => return Err("invalid height"),
Some(height) => match height.try_into() {
- Err(_) => {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid height"))
- },
+ Err(_) => return Err("invalid height"),
Ok(height) => Some(height),
},
},
- _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON number")),
+ _ => return Err("expected JSON number"),
};
Ok((hash, height))
@@ -233,22 +226,18 @@ impl TryInto<(BlockHash, Option<u32>)> for JsonResponse {
}
impl TryInto<Txid> for JsonResponse {
- type Error = io::Error;
- fn try_into(self) -> io::Result<Txid> {
- let hex_data = self
- .0
- .as_str()
- .ok_or(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string"))?;
- Txid::from_str(hex_data)
- .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))
+ type Error = String;
+ fn try_into(self) -> Result<Txid, String> {
+ let hex_data = self.0.as_str().ok_or_else(|| "expected JSON string".to_string())?;
+ Txid::from_str(hex_data).map_err(|err| err.to_string())
}
}
/// Converts a JSON value into a transaction. WATCH OUT! this cannot be used for zero-input transactions
/// (e.g. createrawtransaction). See <https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/197>
impl TryInto<Transaction> for JsonResponse {
- type Error = io::Error;
- fn try_into(self) -> io::Result<Transaction> {
+ type Error = String;
+ fn try_into(self) -> Result<Transaction, String> {
let hex_tx = if self.0.is_object() {
// result is json encoded
match &self.0["hex"] {
@@ -262,10 +251,7 @@ impl TryInto<Transaction> for JsonResponse {
_ => "Unknown error",
};
- return Err(io::Error::new(
- io::ErrorKind::InvalidData,
- format!("transaction couldn't be signed. {}", reason),
- ));
+ return Err(format!("transaction couldn't be signed. {}", reason));
} else {
hex_data
}
@@ -274,7 +260,7 @@ impl TryInto<Transaction> for JsonResponse {
_ => hex_data,
},
_ => {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string"));
+ return Err("expected JSON string".to_string());
},
}
} else {
@@ -282,15 +268,15 @@ impl TryInto<Transaction> for JsonResponse {
match self.0.as_str() {
Some(hex_tx) => hex_tx,
None => {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string"));
+ return Err("expected JSON string".to_string());
},
}
};
match Vec::<u8>::from_hex(hex_tx) {
- Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")),
+ Err(_) => Err("invalid hex data".to_string()),
Ok(tx_data) => match encode::deserialize(&tx_data) {
- Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid transaction")),
+ Err(_) => Err("invalid transaction".to_string()),
Ok(tx) => Ok(tx),
},
}
@@ -298,16 +284,13 @@ impl TryInto<Transaction> for JsonResponse {
}
impl TryInto<BlockHash> for JsonResponse {
- type Error = io::Error;
+ type Error = &'static str;
- fn try_into(self) -> io::Result<BlockHash> {
+ fn try_into(self) -> Result<BlockHash, &'static str> {
match self.0.as_str() {
- None => Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")),
- Some(hex_data) if hex_data.len() != 64 => {
- Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hash length"))
- },
- Some(hex_data) => BlockHash::from_str(hex_data)
- .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")),
+ None => Err("expected JSON string"),
+ Some(hex_data) if hex_data.len() != 64 => Err("invalid hash length"),
+ Some(hex_data) => BlockHash::from_str(hex_data).map_err(|_| "invalid hex data"),
}
}
}
@@ -322,24 +305,21 @@ pub(crate) struct GetUtxosResponse {
#[cfg(feature = "rest-client")]
impl TryInto<GetUtxosResponse> for JsonResponse {
- type Error = io::Error;
+ type Error = &'static str;
- fn try_into(self) -> io::Result<GetUtxosResponse> {
- let obj_err = || io::Error::new(io::ErrorKind::InvalidData, "expected an object");
- let bitmap_err = || io::Error::new(io::ErrorKind::InvalidData, "missing bitmap field");
- let bitstr_err = || io::Error::new(io::ErrorKind::InvalidData, "bitmap should be an str");
+ fn try_into(self) -> Result<GetUtxosResponse, &'static str> {
let bitmap_str = self
.0
.as_object()
- .ok_or_else(obj_err)?
+ .ok_or("expected an object")?
.get("bitmap")
- .ok_or_else(bitmap_err)?
+ .ok_or("missing bitmap field")?
.as_str()
- .ok_or_else(bitstr_err)?;
+ .ok_or("bitmap should be an str")?;
let mut hit_bitmap_nonempty = false;
for c in bitmap_str.chars() {
if c < '0' || c > '9' {
- return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid byte"));
+ return Err("invalid byte");
}
if c > '0' {
hit_bitmap_nonempty = true;
@@ -381,8 +361,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!(42));
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "unexpected JSON type");
+ assert_eq!(e, "unexpected JSON type");
},
Ok(_) => panic!("Expected error"),
}
@@ -393,8 +372,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!([42]));
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
+ assert_eq!(e, "expected JSON object");
},
Ok(_) => panic!("Expected error"),
}
@@ -411,8 +389,7 @@ pub(crate) mod tests {
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid header data");
+ assert_eq!(e, "invalid header data");
},
Ok(_) => panic!("Expected error"),
}
@@ -429,8 +406,7 @@ pub(crate) mod tests {
match TryInto::<BlockHeaderData>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid header data");
+ assert_eq!(e, "invalid header data");
},
Ok(_) => panic!("Expected error"),
}
@@ -524,8 +500,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!({ "result": "foo" }));
match TryInto::<Block>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string");
+ assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
@@ -536,8 +511,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("foobar"));
match TryInto::<Block>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data");
+ assert_eq!(e, "invalid hex data");
},
Ok(_) => panic!("Expected error"),
}
@@ -548,8 +522,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("abcd"));
match TryInto::<Block>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid block data");
+ assert_eq!(e, "invalid block data");
},
Ok(_) => panic!("Expected error"),
}
@@ -570,8 +543,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("foo"));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object");
+ assert_eq!(e, "expected JSON object");
},
Ok(_) => panic!("Expected error"),
}
@@ -582,8 +554,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!({ "bestblockhash": 42 }));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string");
+ assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
@@ -594,8 +565,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!({ "bestblockhash": "foobar"} ));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data");
+ assert_eq!(e, "invalid hex data");
},
Ok(_) => panic!("Expected error"),
}
@@ -625,8 +595,7 @@ pub(crate) mod tests {
}));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON number");
+ assert_eq!(e, "expected JSON number");
},
Ok(_) => panic!("Expected error"),
}
@@ -641,8 +610,7 @@ pub(crate) mod tests {
}));
match TryInto::<(BlockHash, Option<u32>)>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid height");
+ assert_eq!(e, "invalid height");
},
Ok(_) => panic!("Expected error"),
}
@@ -669,8 +637,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!({ "result": "foo" }));
match TryInto::<Txid>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string");
+ assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
@@ -681,8 +648,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("foobar"));
match TryInto::<Txid>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "failed to parse hex");
+ assert_eq!(e, "failed to parse hex");
},
Ok(_) => panic!("Expected error"),
}
@@ -693,8 +659,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("abcd"));
match TryInto::<Txid>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "failed to parse hex");
+ assert_eq!(e, "failed to parse hex");
},
Ok(_) => panic!("Expected error"),
}
@@ -714,9 +679,8 @@ pub(crate) mod tests {
fn into_txid_from_bitcoind_rpc_json_response() {
let mut rpc_response = serde_json::json!(
{"error": "", "id": "770", "result": "7934f775149929a8b742487129a7c3a535dfb612f0b726cc67bc10bc2628f906"}
-
);
- let r: io::Result<Txid> =
+ let r: Result<Txid, String> =
JsonResponse(rpc_response.get_mut("result").unwrap().take()).try_into();
assert_eq!(
r.unwrap().to_string(),
@@ -736,8 +700,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("foobar"));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data");
+ assert_eq!(e, "invalid hex data");
},
Ok(_) => panic!("Expected error"),
}
@@ -748,8 +711,7 @@ pub(crate) mod tests {
let response = JsonResponse(Value::Number(Number::from_f64(1.0).unwrap()));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string");
+ assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
@@ -760,8 +722,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!("abcd"));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "invalid transaction");
+ assert_eq!(e, "invalid transaction");
},
Ok(_) => panic!("Expected error"),
}
@@ -797,8 +758,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!({ "error": "foo" }));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string");
+ assert_eq!(e, "expected JSON string");
},
Ok(_) => panic!("Expected error"),
}
@@ -809,12 +769,7 @@ pub(crate) mod tests {
let response = JsonResponse(serde_json::json!({ "hex": "foo", "complete": false }));
match TryInto::<Transaction>::try_into(response) {
Err(e) => {
- assert_eq!(e.kind(), io::ErrorKind::InvalidData);
- assert!(e
- .get_ref()
- .unwrap()
- .to_string()
- .contains("transaction couldn't be signed"));
+ assert!(e.contains("transaction couldn't be signed"));
},
Ok(_) => panic!("Expected error"),
}
diff --git a/lightning-block-sync/src/http.rs b/lightning-block-sync/src/http.rs
index 29cc425..f473849 100644
--- a/lightning-block-sync/src/http.rs
+++ b/lightning-block-sync/src/http.rs
@@ -6,9 +6,40 @@ use serde_json;
#[cfg(feature = "tokio")]
use bitreq::RequestExt;
+use std::convert::Infallible;
use std::convert::TryFrom;
use std::fmt;
+/// Trait for converting parse errors into a String message.
+pub trait ToParseErrorMessage {
+ /// Converts a parse error into a human-readable message.
+ fn to_parse_error_message(self) -> String;
+}
+
+impl ToParseErrorMessage for Infallible {
+ fn to_parse_error_message(self) -> String {
+ match self {}
+ }
+}
+
+impl ToParseErrorMessage for () {
+ fn to_parse_error_message(self) -> String {
+ "invalid data".to_string()
+ }
+}
+
+impl ToParseErrorMessage for &'static str {
+ fn to_parse_error_message(self) -> String {
+ self.to_string()
+ }
+}
+
+impl ToParseErrorMessage for String {
+ fn to_parse_error_message(self) -> String {
+ self
+ }
+}
+
/// Timeout for requests in seconds. This is set to a high value as it is not uncommon for Bitcoin
/// Core to be blocked waiting on UTXO cache flushes for upwards of 10 minutes on slow devices
/// (e.g. RPis with SSDs over USB).
@@ -26,7 +57,7 @@ pub enum HttpClientError {
/// HTTP error response (non-2xx status code)
Http(HttpError),
/// Response parsing/conversion error
- Io(std::io::Error),
+ Parse(String),
}
impl std::error::Error for HttpClientError {
@@ -34,7 +65,7 @@ impl std::error::Error for HttpClientError {
match self {
HttpClientError::Transport(e) => Some(e),
HttpClientError::Http(e) => Some(e),
- HttpClientError::Io(e) => Some(e),
+ HttpClientError::Parse(_) => None,
}
}
}
@@ -44,17 +75,11 @@ impl fmt::Display for HttpClientError {
match self {
HttpClientError::Transport(e) => write!(f, "transport error: {}", e),
HttpClientError::Http(e) => write!(f, "HTTP error: {}", e),
- HttpClientError::Io(e) => write!(f, "Response parsing/conversion error: {}", e),
+ HttpClientError::Parse(e) => write!(f, "response parsing error: {}", e),
}
}
}
-impl From<std::io::Error> for HttpClientError {
- fn from(e: std::io::Error) -> Self {
- HttpClientError::Io(e)
- }
-}
-
impl From<bitreq::Error> for HttpClientError {
fn from(e: bitreq::Error) -> Self {
HttpClientError::Transport(e)
@@ -97,7 +122,8 @@ impl HttpClient {
#[allow(dead_code)]
pub async fn get<F>(&self, uri: &str) -> Result<F, HttpClientError>
where
- F: TryFrom<Vec<u8>, Error = std::io::Error>,
+ F: TryFrom<Vec<u8>>,
+ <F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage,
{
let url = format!("{}{}", self.base_url, uri);
let request = bitreq::get(url)
@@ -106,7 +132,7 @@ impl HttpClient {
#[cfg(feature = "tokio")]
let request = request.with_pipelining();
let response_body = self.send_request(request).await?;
- F::try_from(response_body).map_err(HttpClientError::Io)
+ F::try_from(response_body).map_err(|e| HttpClientError::Parse(e.to_parse_error_message()))
}
/// Sends a `POST` request for a resource identified by `uri` using the given HTTP
@@ -119,7 +145,8 @@ impl HttpClient {
&self, uri: &str, auth: &str, content: serde_json::Value,
) -> Result<F, HttpClientError>
where
- F: TryFrom<Vec<u8>, Error = std::io::Error>,
+ F: TryFrom<Vec<u8>>,
+ <F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage,
{
let url = format!("{}{}", self.base_url, uri);
let request = bitreq::post(url)
@@ -131,7 +158,7 @@ impl HttpClient {
#[cfg(feature = "tokio")]
let request = request.with_pipelining();
let response_body = self.send_request(request).await?;
- F::try_from(response_body).map_err(HttpClientError::Io)
+ F::try_from(response_body).map_err(|e| HttpClientError::Parse(e.to_parse_error_message()))
}
/// Sends an HTTP request message and reads the response, returning its body.
@@ -178,19 +205,19 @@ pub struct JsonResponse(pub serde_json::Value);
/// Interprets bytes from an HTTP response body as binary data.
impl TryFrom<Vec<u8>> for BinaryResponse {
- type Error = std::io::Error;
+ type Error = Infallible;
- fn try_from(bytes: Vec<u8>) -> std::io::Result<Self> {
+ fn try_from(bytes: Vec<u8>) -> Result<Self, Infallible> {
Ok(BinaryResponse(bytes))
}
}
/// Interprets bytes from an HTTP response body as a JSON value.
impl TryFrom<Vec<u8>> for JsonResponse {
- type Error = std::io::Error;
+ type Error = String;
- fn try_from(bytes: Vec<u8>) -> std::io::Result<Self> {
- Ok(JsonResponse(serde_json::from_slice(&bytes)?))
+ fn try_from(bytes: Vec<u8>) -> Result<Self, String> {
+ serde_json::from_slice(&bytes).map(JsonResponse).map_err(|e| e.to_string())
}
}
diff --git a/lightning-block-sync/src/rest.rs b/lightning-block-sync/src/rest.rs
index 0ea9389..cdcf842 100644
--- a/lightning-block-sync/src/rest.rs
+++ b/lightning-block-sync/src/rest.rs
@@ -3,7 +3,7 @@
use crate::convert::GetUtxosResponse;
use crate::gossip::UtxoSource;
-use crate::http::{BinaryResponse, HttpClient, HttpClientError, JsonResponse};
+use crate::http::{BinaryResponse, HttpClient, HttpClientError, JsonResponse, ToParseErrorMessage};
use crate::{BlockData, BlockHeaderData, BlockSource, BlockSourceResult};
use bitcoin::hash_types::BlockHash;
@@ -29,10 +29,13 @@ impl RestClient {
/// Requests a resource encoded in `F` format and interpreted as type `T`.
pub async fn request_resource<F, T>(&self, resource_path: &str) -> Result<T, HttpClientError>
where
- F: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error>,
+ F: TryFrom<Vec<u8>> + TryInto<T>,
+ <F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage,
+ <F as TryInto<T>>::Error: ToParseErrorMessage,
{
let uri = format!("/{}", resource_path);
- self.client.get::<F>(&uri).await?.try_into().map_err(HttpClientError::Io)
+ let response = self.client.get::<F>(&uri).await?;
+ response.try_into().map_err(|e| HttpClientError::Parse(e.to_parse_error_message()))
}
}
@@ -91,21 +94,15 @@ impl UtxoSource for RestClient {
mod tests {
use super::*;
use crate::http::client_tests::{HttpServer, MessageBody};
- use crate::http::BinaryResponse;
use bitcoin::hashes::Hash;
/// Parses binary data as a string-encoded `u32`.
impl TryInto<u32> for BinaryResponse {
- type Error = std::io::Error;
-
- fn try_into(self) -> std::io::Result<u32> {
- match std::str::from_utf8(&self.0) {
- Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
- Ok(s) => match u32::from_str_radix(s, 10) {
- Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
- Ok(n) => Ok(n),
- },
- }
+ type Error = String;
+
+ fn try_into(self) -> Result<u32, String> {
+ let s = std::str::from_utf8(&self.0).map_err(|e| e.to_string())?;
+ u32::from_str_radix(s, 10).map_err(|e| e.to_string())
}
}
@@ -127,7 +124,7 @@ mod tests {
let client = RestClient::new(server.endpoint());
match client.request_resource::<BinaryResponse, u32>("/").await {
- Err(HttpClientError::Io(_)) => {},
+ Err(HttpClientError::Parse(_)) => {},
Err(e) => panic!("Unexpected error type: {:?}", e),
Ok(_) => panic!("Expected error"),
}
diff --git a/lightning-block-sync/src/rpc.rs b/lightning-block-sync/src/rpc.rs
index c81d7f2..bfa1b31 100644
--- a/lightning-block-sync/src/rpc.rs
+++ b/lightning-block-sync/src/rpc.rs
@@ -2,7 +2,7 @@
//! endpoint.
use crate::gossip::UtxoSource;
-use crate::http::{HttpClient, HttpClientError, JsonResponse};
+use crate::http::{HttpClient, HttpClientError, JsonResponse, ToParseErrorMessage};
use crate::{BlockData, BlockHeaderData, BlockSource, BlockSourceResult};
use bitcoin::hash_types::BlockHash;
@@ -106,7 +106,8 @@ impl RpcClient {
&self, method: &str, params: &[serde_json::Value],
) -> Result<T, RpcClientError>
where
- JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error>,
+ JsonResponse: TryInto<T>,
+ <JsonResponse as TryInto<T>>::Error: ToParseErrorMessage,
{
let content = serde_json::json!({
"method": method,
@@ -148,7 +149,7 @@ impl RpcClient {
JsonResponse(result)
.try_into()
- .map_err(|e: std::io::Error| RpcClientError::InvalidData(e.to_string()))
+ .map_err(|e| RpcClientError::InvalidData(e.to_parse_error_message()))
}
}
@@ -215,11 +216,11 @@ mod tests {
/// Converts a JSON value into `u64`.
impl TryInto<u64> for JsonResponse {
- type Error = std::io::Error;
+ type Error = &'static str;
- fn try_into(self) -> std::io::Result<u64> {
+ fn try_into(self) -> Result<u64, &'static str> {
match self.0.as_u64() {
- None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")),
+ None => Err("not a number"),
Some(n) => Ok(n),
}
}
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.