plugins: lsps: refactor jsonrpc error
What changed, and why it matters
This is a code cleanup change inside an experimental Lightning Service Provider (LSP) plugin. It renames and reorganizes error types so that the JSON-RPC transport code no longer depends on the higher-level plugin code. There is no direct security fix here; it is a refactoring to make the dependency structure cleaner. The change does remove one small oddity where a JSON serialization failure was being reported as an internal JSON-RPC error instead of a plain serialization error, but that is a correctness improvement, not a vulnerability patch.
No security action required. Treat as normal code-quality refactoring. Reviewers may want to confirm that the new error mappings do not accidentally drop error context used by callers or monitoring.
Security signals we found
Refactoring only: no input validation, cryptographic, or authorization logic changed
Error-mapping cleanup removes an incorrect internal_error conversion for serialization failures
No new dependencies, unsafe code, or network-facing behavior introduced
Tests updated only to match renamed error variants
Evidence from the diff
The commit refactors the error enums in plugins/lsps-plugin. The proto::jsonrpc::Error enum is simplified: Json/Transport/Other variants are replaced with ParseResponse/ParseJsonResponse/RpcError/Other, and the dependency on jsonrpc::client::TransportError is removed. Conversely, jsonrpc::client::Error gains JsonRpcError, ParseRequest, Timeout and Internal variants and becomes the top-level transport error. jsonrpc::server and lsps0::transport are updated to use the new types. A notable behavior change is in JsonRpcServer::write_error, where serde_json::to_vec errors are now propagated via ?/From instead of being converted to RpcError::internal_error. Overall this is architectural refactoring with minor error-mapping corrections.
Changed components
plugins/lsps-plugin/src/jsonrpc/client.rsplugins/lsps-plugin/src/jsonrpc/server.rsplugins/lsps-plugin/src/lsps0/transport.rsplugins/lsps-plugin/src/proto/jsonrpc.rsplugins/lsps-plugin/src/service.rsInspect captured patch +71 / −64
diff --git a/plugins/lsps-plugin/src/jsonrpc/client.rs b/plugins/lsps-plugin/src/jsonrpc/client.rs
index 5bc27f65..350d815c 100644
--- a/plugins/lsps-plugin/src/jsonrpc/client.rs
+++ b/plugins/lsps-plugin/src/jsonrpc/client.rs
@@ -1,5 +1,5 @@
use crate::proto::jsonrpc::{
- Error, JsonRpcRequest, JsonRpcResponse, RequestObject, ResponseObject, Result,
+ Error as JsonRpcError, JsonRpcRequest, JsonRpcResponse, RequestObject, ResponseObject,
};
use async_trait::async_trait;
use core::fmt::Debug;
@@ -14,13 +14,28 @@ use thiserror::Error;
/// Transport-specific errors that may occur when sending or receiving JSON-RPC
/// messages.
#[derive(Error, Debug)]
-pub enum TransportError {
+pub enum Error {
#[error("Timeout")]
Timeout,
#[error("Internal error: {0}")]
Internal(String),
+ #[error("Got JSON-RPC error")]
+ JsonRpcError(#[from] JsonRpcError),
+ #[error("Couldn't parse JSON-RPC request")]
+ ParseRequest {
+ #[source]
+ source: serde_json::Error,
+ },
}
+impl From<serde_json::Error> for Error {
+ fn from(value: serde_json::Error) -> Self {
+ Self::ParseRequest { source: value }
+ }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
/// Defines the interface for transporting JSON-RPC messages.
///
/// Implementors of this trait are responsible for actually sending the JSON-RPC
@@ -60,7 +75,7 @@ impl<T: Transport> JsonRpcClient<T> {
id: Some(id.clone().into()),
};
let res_obj = self.send_request(method, &request, id).await?;
- Value::from_response(res_obj)
+ Ok(Value::from_response(res_obj)?)
}
/// Makes a typed JSON-RPC method call with a request object and returns a
@@ -79,7 +94,7 @@ impl<T: Transport> JsonRpcClient<T> {
debug!("Preparing request: method={}, id={}", method, id);
let request = request.into_request(Some(id.clone().into()));
let res_obj = self.send_request(method, &request, id).await?;
- RS::from_response(res_obj)
+ Ok(RS::from_response(res_obj)?)
}
/// Sends a notification with raw JSON parameters (no response expected).
@@ -207,7 +222,7 @@ mod test_json_rpc {
// Check for error first
if let Some(err) = &*self.err {
- return Err(Error::Transport(TransportError::Internal(err.into())));
+ return Err(Error::Internal(err.into()));
}
// Then check for response
@@ -224,7 +239,7 @@ mod test_json_rpc {
// Check for error
if let Some(err) = &*self.err {
- return Err(Error::Transport(TransportError::Internal(err.into())));
+ return Err(Error::Internal(err.into()));
}
Ok(())
@@ -322,17 +337,11 @@ mod test_json_rpc {
.call_typed::<_, DummyResponse>(req)
.await
.expect_err("Expected error response");
- assert!(match res {
- Error::Rpc(rpc_error) => {
- assert_eq!(rpc_error, err_res);
- true
- }
- _ => false,
- });
+ assert!(matches!(res, Error::JsonRpcError(..)));
}
#[tokio::test]
- async fn test_typed_call_w_transport_error() {
+ async fn test_typed_call_w_internal_error() {
let req = DummyCall {
foo: "hello world!".into(),
bar: 13,
@@ -349,12 +358,6 @@ mod test_json_rpc {
.call_typed::<_, DummyResponse>(req)
.await
.expect_err("Expected error response");
- assert!(match res {
- Error::Transport(err) => {
- assert_eq!(err.to_string(), "Internal error: transport error");
- true
- }
- _ => false,
- });
+ assert!(matches!(res, Error::Internal(..)));
}
}
diff --git a/plugins/lsps-plugin/src/jsonrpc/server.rs b/plugins/lsps-plugin/src/jsonrpc/server.rs
index a012f6fa..922a4c25 100644
--- a/plugins/lsps-plugin/src/jsonrpc/server.rs
+++ b/plugins/lsps-plugin/src/jsonrpc/server.rs
@@ -1,4 +1,4 @@
-use crate::proto::jsonrpc::{Result, RpcError, RpcErrorExt as _};
+use crate::proto::jsonrpc::{Result, RpcError};
use async_trait::async_trait;
use log::{debug, trace};
use std::{collections::HashMap, sync::Arc};
@@ -156,7 +156,7 @@ impl JsonRpcServer {
// No need to respond when we don't have an id - it's a notification
if let Some(id) = id {
let err_res = err.clone().into_response(id.into());
- let err_vec = serde_json::to_vec(&err_res).map_err(|e| RpcError::internal_error(e))?;
+ let err_vec = serde_json::to_vec(&err_res)?;
return writer.write(&err_vec).await;
}
Ok(())
diff --git a/plugins/lsps-plugin/src/lsps0/transport.rs b/plugins/lsps-plugin/src/lsps0/transport.rs
index d8b2a8fb..77b648f9 100644
--- a/plugins/lsps-plugin/src/lsps0/transport.rs
+++ b/plugins/lsps-plugin/src/lsps0/transport.rs
@@ -1,6 +1,6 @@
use crate::{
- jsonrpc::client::{Transport, TransportError},
- proto::{jsonrpc::Error, lsps0::LSPS0_MESSAGE_TYPE},
+ jsonrpc::client::{Error, Transport},
+ proto::lsps0::LSPS0_MESSAGE_TYPE,
};
use async_trait::async_trait;
use cln_plugin::Plugin;
@@ -8,6 +8,7 @@ use cln_rpc::{primitives::PublicKey, ClnRpc};
use log::{debug, error, trace};
use serde::{de::Visitor, Deserialize, Serialize};
use std::{
+ array::TryFromSliceError,
collections::HashMap,
path::PathBuf,
str::FromStr,
@@ -191,7 +192,7 @@ impl Bolt8Transport {
timeout: Option<Duration>,
) -> Result<Self, Error> {
let endpoint = cln_rpc::primitives::PublicKey::from_str(endpoint)
- .map_err(|e| TransportError::Internal(e.to_string()))?;
+ .map_err(|e| Error::Internal(e.to_string()))?;
let timeout = timeout.unwrap_or(DEFAULT_TIMEOUT);
Ok(Self {
endpoint,
@@ -205,7 +206,7 @@ impl Bolt8Transport {
async fn connect_to_node(&self) -> Result<ClnRpc, Error> {
ClnRpc::new(&self.rpc_path)
.await
- .map_err(|e| Error::Transport(TransportError::Internal(e.to_string())))
+ .map_err(|e| Error::Internal(e.to_string()))
}
/// Sends a custom message to the destination node.
@@ -220,10 +221,8 @@ impl Bolt8Transport {
) -> Result<CustomMsg, Error> {
tokio::time::timeout(self.request_timeout, rx.recv())
.await
- .map_err(|_| Error::Transport(TransportError::Timeout))?
- .ok_or(Error::Transport(TransportError::Internal(String::from(
- "Channel unexpectedly closed",
- ))))
+ .map_err(|_| Error::Timeout)?
+ .ok_or(Error::Internal(String::from("Channel unexpectedly closed")))
}
}
@@ -246,11 +245,7 @@ pub async fn send_custommsg(
client
.call_typed(&request)
.await
- .map_err(|e| {
- Error::Transport(TransportError::Internal(format!(
- "Failed to send custom msg: {e}"
- )))
- })
+ .map_err(|e| Error::Internal(format!("Failed to send custom msg: {e}")))
.map(|r| {
trace!("Successfully queued custom msg: {}", r.status);
()
@@ -282,18 +277,18 @@ impl Transport for Bolt8Transport {
let res = self.wait_for_response(rx).await?;
if res.message_type != LSPS0_MESSAGE_TYPE {
- return Err(Error::Transport(TransportError::Internal(format!(
+ return Err(Error::Internal(format!(
"unexpected response message type: expected {}, got {}",
LSPS0_MESSAGE_TYPE, res.message_type
- ))));
+ )));
}
core::str::from_utf8(&res.payload)
.map_err(|e| {
- Error::Transport(TransportError::Internal(format!(
+ Error::Internal(format!(
"failed to decode msg payload {:?}: {}",
res.payload, e
- )))
+ ))
})
.map(|s| s.into())
}
@@ -332,15 +327,17 @@ impl FromStr for CustomMsg {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- let bytes = hex::decode(s).map_err(Error::other)?;
+ let bytes = hex::decode(s).map_err(|e| Error::Internal(e.to_string()))?;
if bytes.len() < 2 {
- return Err(Error::other(
- "hex string too short to contain a valid message_type",
+ return Err(Error::Internal(
+ "hex string too short to contain a valid message_type".to_string(),
));
}
- let message_type_bytes: [u8; 2] = bytes[..2].try_into().map_err(Error::other)?;
+ let message_type_bytes: [u8; 2] = bytes[..2]
+ .try_into()
+ .map_err(|e: TryFromSliceError| Error::Internal(e.to_string()))?;
let message_type = u16::from_be_bytes(message_type_bytes);
let payload = bytes[2..].to_owned();
Ok(CustomMsg {
diff --git a/plugins/lsps-plugin/src/proto/jsonrpc.rs b/plugins/lsps-plugin/src/proto/jsonrpc.rs
index 5f5190df..ef4c6c9f 100644
--- a/plugins/lsps-plugin/src/proto/jsonrpc.rs
+++ b/plugins/lsps-plugin/src/proto/jsonrpc.rs
@@ -3,8 +3,6 @@ use serde_json::{self, Value};
use std::fmt;
use thiserror::Error;
-use crate::jsonrpc::client::TransportError;
-
// Constants for JSON-RPC error codes.
pub const PARSE_ERROR: i64 = -32700;
pub const INVALID_REQUEST: i64 = -32600;
@@ -17,15 +15,18 @@ pub const INTERNAL_ERROR: i64 = -32603;
/// Encapsulates various error conditions that may occur during JSON-RPC
/// operations, including serialization errors, transport issues, and
/// protocol-specific errors.
-#[derive(Error, Debug)]
+#[derive(Debug, Error)]
pub enum Error {
- #[error("JSON error: {0}")]
- Json(#[from] serde_json::Error),
- #[error("RPC error: {0}")]
- Rpc(#[from] RpcError),
- #[error("Transport error: {0}")]
- Transport(#[from] TransportError),
- #[error("Other error: {0}")]
+ #[error("Failed to parse JSON-RPC response")]
+ ParseResponse,
+ #[error("Failed to parse JSON-RPC response")]
+ ParseJsonResponse {
+ #[source]
+ source: serde_json::Error,
+ },
+ #[error("Got JSON-RPC error")]
+ RpcError(#[from] RpcError),
+ #[error("Internal error: {0}")]
Other(String),
}
@@ -35,6 +36,12 @@ impl Error {
}
}
+impl From<serde_json::Error> for Error {
+ fn from(value: serde_json::Error) -> Self {
+ Self::ParseJsonResponse { source: value }
+ }
+}
+
/// Convenience type alias for Result with the JSON-RPC Error type.
pub type Result<T> = std::result::Result<T, Error>;
@@ -84,10 +91,8 @@ where
{
match (resp.result, resp.error) {
(Some(result), None) => Ok(result),
- (None, Some(error)) => Err(Error::Rpc(error)),
- _ => Err(Error::Rpc(RpcError::internal_error(
- "not a valid json respone",
- ))),
+ (None, Some(error)) => Err(Error::RpcError(error)),
+ _ => Err(Error::ParseResponse),
}
}
}
@@ -438,7 +443,7 @@ mod test_message_serialization {
let response = response_object.into_inner();
let err = response.unwrap_err();
match err {
- Error::Rpc(err) => {
+ Error::RpcError(err) => {
assert_eq!(err.code, -32099);
assert_eq!(err.message, "something bad happened");
assert_eq!(
diff --git a/plugins/lsps-plugin/src/service.rs b/plugins/lsps-plugin/src/service.rs
index 2500c5de..1528a2b2 100644
--- a/plugins/lsps-plugin/src/service.rs
+++ b/plugins/lsps-plugin/src/service.rs
@@ -1,6 +1,5 @@
use anyhow::{anyhow, bail};
use async_trait::async_trait;
-use cln_lsps::jsonrpc::client::TransportError;
use cln_lsps::jsonrpc::server::JsonRpcResponseWriter;
use cln_lsps::jsonrpc::server::JsonRpcServer;
use cln_lsps::lsps0::handler::Lsps0ListProtocolsHandler;
@@ -8,7 +7,7 @@ use cln_lsps::lsps0::transport::{self, CustomMsg};
use cln_lsps::lsps2;
use cln_lsps::lsps2::cln::{HtlcAcceptedRequest, HtlcAcceptedResponse};
use cln_lsps::lsps2::handler::{ClnApiRpc, HtlcAcceptedHookHandler};
-use cln_lsps::proto::jsonrpc::{Error, JsonRpcRequest as _, Result as RpcResult};
+use cln_lsps::proto::jsonrpc::{Error as JError, JsonRpcRequest as _};
use cln_lsps::proto::lsps0::{Lsps0listProtocolsRequest, LSPS0_MESSAGE_TYPE};
use cln_lsps::proto::lsps2::{Lsps2BuyRequest, Lsps2GetInfoRequest};
use cln_lsps::util::wrap_payload_with_peer_id;
@@ -185,10 +184,13 @@ pub struct LspsResponseWriter {
#[async_trait]
impl JsonRpcResponseWriter for LspsResponseWriter {
- async fn write(&mut self, payload: &[u8]) -> RpcResult<()> {
+ async fn write(&mut self, payload: &[u8]) -> std::result::Result<(), JError> {
let mut client = cln_rpc::ClnRpc::new(&self.rpc_path)
.await
- .map_err(|e| Error::Transport(TransportError::Internal(e.to_string())))?;
- transport::send_custommsg(&mut client, payload.to_vec(), self.peer_id).await
+ .map_err(|e| JError::Other(e.to_string()))?;
+
+ transport::send_custommsg(&mut client, payload.to_vec(), self.peer_id)
+ .await
+ .map_err(|e| JError::Other(e.to_string()))
}
}
Why this scored 17/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.