use std::io;
use std::sync::Arc;
use std::time::Duration;
use serde::Serialize;
use serde_json::{Value, json};
use tokio::io::{AsyncBufRead, AsyncWrite};
use tokio::task::JoinHandle;
use crate::jsonrpc_client::{self, Incoming, Inner, Protocol, RpcErr};
#[cfg(test)]
use crate::jsonrpc_framing::{decode_frame, encode_frame};
#[derive(Debug, thiserror::Error)]
pub enum RpcError {
#[error("RPC error {code}: {message}")]
Server { code: i64, message: String },
#[error("connection closed before response arrived")]
ConnectionClosed,
#[error("request timed out after {0:?}")]
Timeout(Duration),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("serialization error: {0}")]
Serialize(#[from] serde_json::Error),
}
impl RpcErr for RpcError {
fn connection_closed() -> Self {
RpcError::ConnectionClosed
}
fn timeout(duration: Duration) -> Self {
RpcError::Timeout(duration)
}
}
pub type NotificationHandler = Box<dyn Fn(Value) + Send + Sync>;
struct LspProtocol;
impl Protocol for LspProtocol {
type Error = RpcError;
fn name() -> &'static str {
"lsp"
}
fn build_request(id: u64, method: &str, params: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })
}
fn build_notification(_id: u64, method: &str, params: Value) -> Value {
json!({ "jsonrpc": "2.0", "method": method, "params": params })
}
fn classify(msg: &Value) -> Incoming<RpcError> {
let id_value = msg.get("id").cloned();
let id_num = id_value.as_ref().and_then(|v| v.as_u64());
let id_str = id_value.as_ref().and_then(|v| v.as_str()).map(String::from);
let id = id_num.or_else(|| id_str.as_ref().and_then(|s| s.parse().ok()));
let method = msg.get("method").and_then(|v| v.as_str()).map(String::from);
match (id, method) {
(Some(id), None) => Incoming::Response {
id,
result: response_result(msg),
},
(None, Some(method)) => Incoming::Notify {
key: method,
body: msg.get("params").cloned().unwrap_or(Value::Null),
},
(Some(id), Some(_method)) => {
Incoming::ReverseRequest {
ack: json!({ "jsonrpc": "2.0", "id": id, "result": Value::Null }),
}
}
(None, None) => Incoming::Ignore,
}
}
}
fn response_result(msg: &Value) -> Result<Value, RpcError> {
if let Some(err) = msg.get("error") {
let code = err.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
let message = err
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("(no message)")
.to_string();
Err(RpcError::Server { code, message })
} else {
Ok(msg.get("result").cloned().unwrap_or(Value::Null))
}
}
#[derive(Clone)]
pub struct RpcClient {
inner: Arc<Inner<RpcError>>,
}
impl RpcClient {
pub fn new<R, W>(reader: R, writer: W) -> (Self, JoinHandle<io::Result<()>>)
where
R: AsyncBufRead + Send + Unpin + 'static,
W: AsyncWrite + Send + Unpin + 'static,
{
let (inner, task) = jsonrpc_client::new::<LspProtocol, R, W>(reader, writer);
(RpcClient { inner }, task)
}
pub async fn request<P, R>(
&self,
method: &str,
params: P,
request_timeout: Duration,
) -> Result<R, RpcError>
where
P: Serialize,
R: serde::de::DeserializeOwned,
{
jsonrpc_client::request::<LspProtocol, P, R>(&self.inner, method, params, request_timeout)
.await
}
pub async fn notify<P>(&self, method: &str, params: P) -> Result<(), RpcError>
where
P: Serialize,
{
jsonrpc_client::notify::<LspProtocol, P>(&self.inner, method, params).await
}
pub async fn on_notification(&self, method: &str, handler: NotificationHandler) {
jsonrpc_client::register_notification(&self.inner, method, Arc::from(handler)).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tokio::io::BufReader;
fn pair() -> (
RpcClient,
JoinHandle<io::Result<()>>,
tokio::io::ReadHalf<tokio::io::DuplexStream>, // server reads what the client sent
tokio::io::WriteHalf<tokio::io::DuplexStream>, // server writes; client reads
) {
let (client_in, server_out) = tokio::io::duplex(4096); let (server_in, client_out) = tokio::io::duplex(4096); let (client_reader, _) = tokio::io::split(client_in);
let (_, client_writer) = tokio::io::split(client_out);
let (server_reader, _) = tokio::io::split(server_in);
let (_, server_writer) = tokio::io::split(server_out);
let (client, task) = RpcClient::new(BufReader::new(client_reader), client_writer);
(client, task, server_reader, server_writer)
}
async fn read_client_frame<R>(reader: &mut R) -> Value
where
R: tokio::io::AsyncReadExt + Unpin + tokio::io::AsyncBufRead,
{
let bytes = decode_frame(reader).await.unwrap();
serde_json::from_slice(&bytes).unwrap()
}
#[tokio::test]
async fn request_round_trips_and_resolves_with_result() {
let (client, _task, server_reader, mut server_writer) = pair();
let mut server_reader = BufReader::new(server_reader);
let req_task = tokio::spawn({
let client = client.clone();
async move {
client
.request::<_, Value>("ping", json!({"q": 1}), Duration::from_secs(2))
.await
}
});
let req = read_client_frame(&mut server_reader).await;
assert_eq!(req["method"], "ping");
assert_eq!(req["params"]["q"], 1);
let id = req["id"].as_u64().unwrap();
let resp = json!({"jsonrpc": "2.0", "id": id, "result": {"pong": true}});
let bytes = serde_json::to_vec(&resp).unwrap();
encode_frame(&mut server_writer, &bytes).await.unwrap();
let got = req_task.await.unwrap().unwrap();
assert_eq!(got, json!({"pong": true}));
}
#[tokio::test]
async fn request_returns_server_error_when_response_has_error() {
let (client, _task, server_reader, mut server_writer) = pair();
let mut server_reader = BufReader::new(server_reader);
let req_task = tokio::spawn({
let client = client.clone();
async move {
client
.request::<_, Value>("explode", json!({}), Duration::from_secs(2))
.await
}
});
let req = read_client_frame(&mut server_reader).await;
let id = req["id"].as_u64().unwrap();
let resp = json!({
"jsonrpc": "2.0",
"id": id,
"error": {"code": -32601, "message": "method not found"}
});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp).unwrap())
.await
.unwrap();
let err = req_task.await.unwrap().unwrap_err();
match err {
RpcError::Server { code, message } => {
assert_eq!(code, -32601);
assert!(message.contains("method not found"));
}
other => panic!("expected Server error, got {other:?}"),
}
}
#[tokio::test]
async fn regression_in_flight_requests_correlated_by_id() {
let (client, _task, server_reader, mut server_writer) = pair();
let mut server_reader = BufReader::new(server_reader);
let a = tokio::spawn({
let c = client.clone();
async move {
c.request::<_, Value>("op", json!({"n": 1}), Duration::from_secs(2))
.await
}
});
let b = tokio::spawn({
let c = client.clone();
async move {
c.request::<_, Value>("op", json!({"n": 2}), Duration::from_secs(2))
.await
}
});
let req1 = read_client_frame(&mut server_reader).await;
let req2 = read_client_frame(&mut server_reader).await;
let id1 = req1["id"].as_u64().unwrap();
let id2 = req2["id"].as_u64().unwrap();
let resp2 = json!({"jsonrpc":"2.0","id":id2,"result":{"answer":"second"}});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp2).unwrap())
.await
.unwrap();
let resp1 = json!({"jsonrpc":"2.0","id":id1,"result":{"answer":"first"}});
encode_frame(&mut server_writer, &serde_json::to_vec(&resp1).unwrap())
.await
.unwrap();
let got_a = a.await.unwrap().unwrap();
let got_b = b.await.unwrap().unwrap();
assert_eq!(got_a["answer"], "first");
assert_eq!(got_b["answer"], "second");
}
#[tokio::test]
async fn regression_request_times_out_when_no_response() {
let (client, _task, _server_reader, _server_writer) = pair();
let err = client
.request::<_, Value>("never", json!({}), Duration::from_millis(50))
.await
.unwrap_err();
assert!(matches!(err, RpcError::Timeout(_)));
}
#[tokio::test]
async fn regression_timeout_clears_pending_entry() {
let (client, _task, _server_reader, _server_writer) = pair();
let _ = client
.request::<_, Value>("never", json!({}), Duration::from_millis(20))
.await;
let pending = client.inner.pending.lock().await;
assert!(pending.is_empty(), "pending must be empty after timeout");
}
#[tokio::test]
async fn notify_sends_payload_without_id() {
let (client, _task, server_reader, _server_writer) = pair();
let mut server_reader = BufReader::new(server_reader);
client
.notify("textDocument/didOpen", json!({"path": "x.rs"}))
.await
.unwrap();
let frame = read_client_frame(&mut server_reader).await;
assert_eq!(frame["method"], "textDocument/didOpen");
assert_eq!(frame["params"]["path"], "x.rs");
assert!(frame.get("id").is_none(), "notifications must not carry id");
}
#[tokio::test]
async fn server_notification_dispatches_to_registered_handler() {
let (client, _task, _server_reader, mut server_writer) = pair();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
client
.on_notification(
"textDocument/publishDiagnostics",
Box::new(move |params| {
let _ = tx.send(params);
}),
)
.await;
let note = json!({
"jsonrpc": "2.0",
"method": "textDocument/publishDiagnostics",
"params": {"uri": "file:///x.rs", "diagnostics": []},
});
encode_frame(&mut server_writer, &serde_json::to_vec(¬e).unwrap())
.await
.unwrap();
let got = tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await
.expect("handler must fire within timeout")
.unwrap();
assert_eq!(got["uri"], "file:///x.rs");
}
#[tokio::test]
async fn regression_server_request_acknowledged_with_null_result() {
let (client, _task, server_reader, mut server_writer) = pair();
let mut server_reader = BufReader::new(server_reader);
let server_req = json!({
"jsonrpc": "2.0",
"id": 999,
"method": "workspace/configuration",
"params": {},
});
encode_frame(
&mut server_writer,
&serde_json::to_vec(&server_req).unwrap(),
)
.await
.unwrap();
let reply = read_client_frame(&mut server_reader).await;
assert_eq!(reply["id"], 999);
assert!(reply["result"].is_null());
assert!(reply.get("error").is_none());
drop(client);
}
#[tokio::test]
async fn regression_in_flight_requests_fail_on_peer_close() {
let (client, task, _server_reader, server_writer) = pair();
let pending = tokio::spawn({
let c = client.clone();
async move {
c.request::<_, Value>("op", json!({}), Duration::from_secs(2))
.await
}
});
drop(server_writer);
let _ = task.await;
let err = pending.await.unwrap().unwrap_err();
assert!(matches!(err, RpcError::ConnectionClosed));
}
#[tokio::test]
async fn request_after_close_returns_closed_error() {
let (client, task, _server_reader, server_writer) = pair();
drop(server_writer);
let _ = task.await;
let err = client
.request::<_, Value>("op", json!({}), Duration::from_secs(1))
.await
.unwrap_err();
assert!(matches!(err, RpcError::ConnectionClosed));
}
#[tokio::test]
async fn malformed_frame_drains_pending_and_marks_closed() {
use tokio::io::AsyncWriteExt;
let (client, task, _server_reader, mut server_writer) = pair();
let pending = tokio::spawn({
let c = client.clone();
async move {
c.request::<_, Value>("op", json!({}), Duration::from_secs(30))
.await
}
});
tokio::task::yield_now().await;
server_writer
.write_all(b"Content-Length: not-a-number\r\n\r\n")
.await
.unwrap();
let got = tokio::time::timeout(Duration::from_secs(5), pending)
.await
.expect("in-flight request should resolve promptly, not wait out its timeout")
.unwrap();
assert!(matches!(got, Err(RpcError::ConnectionClosed)));
let later = client
.request::<_, Value>("op", json!({}), Duration::from_secs(30))
.await;
assert!(matches!(later, Err(RpcError::ConnectionClosed)));
let task_result = tokio::time::timeout(Duration::from_secs(5), task)
.await
.expect("read task should exit, not hang")
.unwrap();
assert!(task_result.is_err());
}
}