Skip to main content

zebra_rpc/server/
rpc_call_compatibility.rs

1//! Compatibility fixes for JSON-RPC remote procedure calls.
2//!
3//! These fixes are applied at the JSON-RPC call level,
4//! after the RPC request is parsed and split into calls.
5
6use jsonrpsee::{
7    server::middleware::rpc::{layer::ResponseFuture, RpcServiceT},
8    MethodResponse,
9};
10use jsonrpsee_types::{ErrorObject, ErrorObjectOwned};
11
12/// JSON-RPC [`FixRpcResponseMiddleware`] with compatibility workarounds.
13///
14/// This middleware makes the following changes to JSON-RPC calls:
15///
16/// ## Make RPC framework response codes match `zcashd`
17///
18/// [`jsonrpsee_types`] returns specific error codes while parsing requests:
19/// <https://docs.rs/jsonrpsee-types/latest/jsonrpsee_types/error/enum.ErrorCode.html>
20///
21/// But these codes are different from `zcashd`, and some RPC clients rely on the exact code.
22/// Specifically, the [`jsonrpsee_types::error::INVALID_PARAMS_CODE`] is different:
23/// <https://docs.rs/jsonrpsee-types/latest/jsonrpsee_types/error/constant.INVALID_PARAMS_CODE.html>
24#[derive(Clone)]
25pub struct FixRpcResponseMiddleware<S> {
26    service: S,
27}
28
29impl<S> FixRpcResponseMiddleware<S> {
30    /// Create a new `FixRpcResponseMiddleware` with the given `service`.
31    pub fn new(service: S) -> Self {
32        Self { service }
33    }
34}
35
36impl<'a, S> RpcServiceT<'a> for FixRpcResponseMiddleware<S>
37where
38    S: RpcServiceT<'a> + Send + Sync + Clone + 'static,
39{
40    type Future = ResponseFuture<futures::future::BoxFuture<'a, jsonrpsee::MethodResponse>>;
41
42    fn call(&self, request: jsonrpsee::types::Request<'a>) -> Self::Future {
43        let service = self.service.clone();
44
45        // The response carries the same id as the request, so keep the id here instead of
46        // parsing it back out of the serialized response.
47        let id = request.id.clone().into_owned();
48
49        ResponseFuture::future(Box::pin(async move {
50            let response = service.call(request).await;
51
52            if response.as_error_code() != Some(jsonrpsee_types::ErrorCode::InvalidParams.code()) {
53                return response;
54            }
55
56            let new_error_code = crate::server::error::LegacyCode::Misc.into();
57            tracing::debug!(
58                "Replacing RPC error code {} with {new_error_code}",
59                jsonrpsee_types::ErrorCode::InvalidParams.code(),
60            );
61
62            // Recover the original error object so only its code changes, preserving its message
63            // and data. If it can't be parsed, return the response unchanged.
64            let Some(error) = serde_json::from_str::<serde_json::Value>(response.as_result())
65                .ok()
66                .and_then(|json| {
67                    serde_json::from_value::<ErrorObjectOwned>(json.get("error")?.clone()).ok()
68                })
69            else {
70                return response;
71            };
72
73            MethodResponse::error(
74                id,
75                ErrorObject::owned(new_error_code, error.message(), error.data()),
76            )
77        }))
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use jsonrpsee::{
84        server::middleware::rpc::RpcServiceT,
85        types::{Id, Request},
86        MethodResponse,
87    };
88    use jsonrpsee_types::{ErrorCode, ErrorObject};
89
90    use crate::server::error::LegacyCode;
91
92    use super::FixRpcResponseMiddleware;
93
94    /// A mock inner service that returns an error with the given code, message and data.
95    #[derive(Clone)]
96    struct ErrorService {
97        code: i32,
98        message: &'static str,
99        data: Option<serde_json::Value>,
100    }
101
102    impl<'a> RpcServiceT<'a> for ErrorService {
103        type Future = std::future::Ready<MethodResponse>;
104
105        fn call(&self, request: Request<'a>) -> Self::Future {
106            let error = ErrorObject::owned(self.code, self.message, self.data.clone());
107            std::future::ready(MethodResponse::error(request.id.into_owned(), error))
108        }
109    }
110
111    fn request() -> Request<'static> {
112        Request::new("getinfo".into(), None, Id::Number(42))
113    }
114
115    /// An `InvalidParams` error has its code replaced with the `zcashd`-compatible `Misc` code,
116    /// while its id, message and data are preserved.
117    #[tokio::test]
118    async fn replaces_invalid_params_code_preserving_id_message_and_data() {
119        let inner = ErrorService {
120            code: ErrorCode::InvalidParams.code(),
121            message: "bad params",
122            data: Some(serde_json::json!({ "detail": "x" })),
123        };
124
125        let response = FixRpcResponseMiddleware::new(inner).call(request()).await;
126
127        assert!(response.is_error());
128        assert_eq!(response.as_error_code(), Some(i32::from(LegacyCode::Misc)));
129
130        let json: serde_json::Value =
131            serde_json::from_str(response.as_result()).expect("response should be valid json");
132        assert_eq!(json["id"], serde_json::json!(42));
133        assert_eq!(
134            json["error"]["code"],
135            serde_json::json!(i32::from(LegacyCode::Misc))
136        );
137        assert_eq!(json["error"]["message"], "bad params");
138        assert_eq!(json["error"]["data"], serde_json::json!({ "detail": "x" }));
139    }
140
141    /// A string request id is preserved through the rewrite. String ids were the case the old
142    /// code (which re-parsed the id from JSON) was most fragile on.
143    #[tokio::test]
144    async fn preserves_string_id() {
145        let inner = ErrorService {
146            code: ErrorCode::InvalidParams.code(),
147            message: "bad params",
148            data: None,
149        };
150
151        let request = Request::new("getinfo".into(), None, Id::Str("curltest".into()));
152        let response = FixRpcResponseMiddleware::new(inner).call(request).await;
153
154        assert_eq!(response.as_error_code(), Some(i32::from(LegacyCode::Misc)));
155        let json: serde_json::Value =
156            serde_json::from_str(response.as_result()).expect("response should be valid json");
157        assert_eq!(json["id"], serde_json::json!("curltest"));
158    }
159
160    /// Errors with any other code pass through unchanged.
161    #[tokio::test]
162    async fn leaves_other_error_codes_unchanged() {
163        let inner = ErrorService {
164            code: ErrorCode::MethodNotFound.code(),
165            message: "nope",
166            data: None,
167        };
168
169        let response = FixRpcResponseMiddleware::new(inner).call(request()).await;
170
171        assert_eq!(
172            response.as_error_code(),
173            Some(ErrorCode::MethodNotFound.code())
174        );
175        let json: serde_json::Value =
176            serde_json::from_str(response.as_result()).expect("response should be valid json");
177        assert_eq!(json["error"]["message"], "nope");
178    }
179}