use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, Mutex as StdMutex, PoisonError};
use lsp_types::{
ErrorCodes, LspErrorCodes, LspRequestMethod, Notification as _, PublishDiagnosticsParams,
};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
use tokio::sync::{Mutex, mpsc, oneshot};
use tokio::task::JoinHandle;
use tokio::time::{Duration, Instant, timeout, timeout_at};
use tracing::{debug, error, trace, warn};
use crate::config::{LanguageId, LspServerConfig, LspSettings, ServerId};
use crate::error::{BackgroundTask, Error, Result};
use crate::lsp::transport::{LspTransport, LspTransportReader};
use crate::lsp::types::{
InboundMessage, JsonRpcError, JsonRpcNotification, JsonRpcOutcome, JsonRpcReply,
JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, LspNotification, RequestId,
};
use crate::lsp::{DropCounter, DropLog, Lane, NotificationSink, ServerMessage};
use crate::redaction::{RedactedText, Redactions};
use crate::util::MAX_ERROR_MESSAGE_CALLER_BYTES;
mod cancel;
mod notification;
use cancel::{InFlightRequest, PendingRequests, Resolution};
use notification::OutboundNotification;
const SERVER_CANCELLED_MAX_RETRIES: u32 = 3;
const SERVER_CANCELLED_INITIAL_DELAY_MS: u64 = 500;
const READER_CHANNEL_CAPACITY: usize = 100;
const READER_TASK_ABORT_GRACE: Duration = Duration::from_secs(1);
pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
pub const MAX_CONSECUTIVE_UNDECODABLE_FRAMES: u32 = 64;
const _: () = assert!(MAX_CONSECUTIVE_UNDECODABLE_FRAMES > 0);
const SHUTDOWN_ABORT_SETTLE: Duration = Duration::from_millis(50);
pub const CONTENT_MODIFIED_RETRY_METHODS: &[LspRequestMethod<'static>] = &[
LspRequestMethod::TextDocumentSignatureHelp,
LspRequestMethod::TextDocumentInlayHint,
LspRequestMethod::TextDocumentCompletion,
LspRequestMethod::TextDocumentPrepareCallHierarchy,
LspRequestMethod::CallHierarchyIncomingCalls,
LspRequestMethod::CallHierarchyOutgoingCalls,
LspRequestMethod::TextDocumentDiagnostic,
LspRequestMethod::TextDocumentHover,
LspRequestMethod::TextDocumentDefinition,
LspRequestMethod::TextDocumentReferences,
LspRequestMethod::TextDocumentImplementation,
LspRequestMethod::TextDocumentTypeDefinition,
LspRequestMethod::TextDocumentDocumentSymbol,
LspRequestMethod::WorkspaceSymbol,
];
const COMPLETION_TIMEOUT_CAP: Duration = Duration::from_secs(10);
const CODE_ACTION_RESOLVE_TIMEOUT_CAP: Duration = Duration::from_secs(10);
fn spawn_reader_task(
mut reader: LspTransportReader,
) -> (JoinHandle<()>, mpsc::Receiver<Result<InboundMessage>>) {
let (tx, rx) = mpsc::channel(READER_CHANNEL_CAPACITY);
let handle = tokio::spawn(async move {
let mut run = UndecodableRun::default();
loop {
let message = reader
.receive()
.await
.and_then(|message| run.observe(message));
let is_err = message.is_err();
if tx.send(message).await.is_err() || is_err {
break;
}
}
});
(handle, rx)
}
#[derive(Debug, Default)]
struct UndecodableRun {
in_a_row: u32,
drops: DropCounter,
}
impl UndecodableRun {
fn observe(&mut self, message: InboundMessage) -> Result<InboundMessage> {
self.observe_at(message, std::time::Instant::now())
}
fn observe_at(
&mut self,
message: InboundMessage,
now: std::time::Instant,
) -> Result<InboundMessage> {
if !message.is_undecodable() {
self.in_a_row = 0;
return Ok(message);
}
self.in_a_row = self.in_a_row.saturating_add(1);
if self.in_a_row >= MAX_CONSECUTIVE_UNDECODABLE_FRAMES {
return Err(Error::LspProtocolError(RedactedText::fixed(
"too many consecutive undecodable messages",
)));
}
if let Some(suppressed) = self.drops.record(now) {
warn!(
"Dropped an undecodable LSP message ({suppressed} others dropped since the last \
warning); further ones are logged at debug for a minute"
);
} else {
debug!(
"Dropped an undecodable LSP message ({} in a row)",
self.in_a_row
);
}
Ok(message)
}
}
#[derive(Debug, Clone, Default)]
pub enum PendingFailure {
#[default]
Terminated,
Restarted(ServerId),
}
impl PendingFailure {
fn error(&self) -> Error {
match self {
Self::Terminated => Error::ServerTerminated,
Self::Restarted(server_id) => Error::ServerRestarted {
server_id: server_id.clone(),
},
}
}
}
#[derive(Debug)]
pub struct LspClient {
config: LspServerConfig,
state: Arc<Mutex<super::ServerState>>,
request_counter: Arc<AtomicI64>,
command_tx: mpsc::Sender<ClientCommand>,
pending_requests: PendingRequests,
closing: Arc<AtomicBool>,
pending_failure: Arc<StdMutex<PendingFailure>>,
receiver_task: Option<JoinHandle<Result<()>>>,
redactions: Arc<Redactions>,
connection_id: ConnectionId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ConnectionId(u64);
impl ConnectionId {
fn next() -> Self {
static NEXT: AtomicU64 = AtomicU64::new(1);
Self(NEXT.fetch_add(1, Ordering::Relaxed))
}
}
impl Clone for LspClient {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
state: Arc::clone(&self.state),
request_counter: Arc::clone(&self.request_counter),
command_tx: self.command_tx.clone(),
pending_requests: self.pending_requests.clone(),
closing: Arc::clone(&self.closing),
pending_failure: Arc::clone(&self.pending_failure),
receiver_task: None,
redactions: Arc::clone(&self.redactions),
connection_id: self.connection_id,
}
}
}
#[derive(Debug)]
struct Lanes {
notifications: NotificationSink,
lifecycle: mpsc::Sender<LspNotification>,
}
#[derive(Debug)]
enum Route<'a> {
Mailbox(&'a NotificationSink, PublishDiagnosticsParams),
Messages(&'a NotificationSink, ServerMessage),
Lifecycle(&'a mpsc::Sender<LspNotification>, LspNotification),
Floor,
}
#[derive(Debug, PartialEq, Eq)]
enum Delivery {
Delivered,
Dropped(Lane),
}
#[cfg(test)]
#[derive(Debug)]
pub struct CommandProbe(mpsc::Receiver<ClientCommand>);
#[cfg(test)]
impl CommandProbe {
pub(crate) fn next_method(&mut self) -> Option<String> {
match self.0.try_recv().ok()? {
ClientCommand::SendRequest { request } => Some(request.method),
ClientCommand::SendNotification { method, .. } => Some(method),
ClientCommand::Shutdown => Some("<shutdown>".to_owned()),
}
}
}
#[derive(Debug)]
enum ClientCommand {
SendRequest { request: JsonRpcRequest },
SendNotification {
method: String,
params: Option<Value>,
},
Shutdown,
}
impl LspClient {
#[must_use]
pub fn new(config: LspServerConfig) -> Self {
let (command_tx, _command_rx) = mpsc::channel(1);
Self {
config,
state: Arc::new(Mutex::new(super::ServerState::Uninitialized)),
request_counter: Arc::new(AtomicI64::new(1)),
command_tx,
pending_requests: PendingRequests::default(),
closing: Arc::default(),
pending_failure: Arc::default(),
receiver_task: None,
redactions: Arc::default(),
connection_id: ConnectionId::next(),
}
}
#[cfg(test)]
pub(crate) fn with_command_channel(
config: LspServerConfig,
capacity: usize,
) -> (Self, CommandProbe) {
let (command_tx, command_rx) = mpsc::channel(capacity);
(
Self {
command_tx,
..Self::new(config)
},
CommandProbe(command_rx),
)
}
#[cfg(test)]
pub(crate) fn from_transport(
config: LspServerConfig,
transport: (LspTransport, LspTransportReader),
) -> Self {
Self::spawn(config, transport, None, Arc::default())
}
pub(crate) fn from_transport_with_notifications(
config: LspServerConfig,
transport: (LspTransport, LspTransportReader),
notification_tx: NotificationSink,
lifecycle_tx: mpsc::Sender<LspNotification>,
redactions: Arc<Redactions>,
) -> Self {
let lanes = Lanes {
notifications: notification_tx,
lifecycle: lifecycle_tx,
};
Self::spawn(config, transport, Some(lanes), redactions)
}
fn spawn(
config: LspServerConfig,
transport: (LspTransport, LspTransportReader),
lanes: Option<Lanes>,
redactions: Arc<Redactions>,
) -> Self {
let pending_requests = PendingRequests::default();
let pending_failure = Arc::<StdMutex<PendingFailure>>::default();
let (command_tx, command_rx) = mpsc::channel(100);
let receiver_task = tokio::spawn(Self::message_loop(
transport,
command_rx,
pending_requests.clone(),
Arc::clone(&pending_failure),
lanes,
Arc::clone(&redactions),
Self::shared_settings(&config),
));
Self {
config,
state: Arc::new(Mutex::new(super::ServerState::Initializing)),
request_counter: Arc::new(AtomicI64::new(1)),
command_tx,
pending_requests,
closing: Arc::default(),
pending_failure,
receiver_task: Some(receiver_task),
redactions,
connection_id: ConnectionId::next(),
}
}
fn shared_settings(config: &LspServerConfig) -> Option<Arc<LspSettings>> {
config.settings.clone().map(Arc::new)
}
pub(crate) const fn connection_id(&self) -> ConnectionId {
self.connection_id
}
pub(crate) const fn redactions(&self) -> &Arc<Redactions> {
&self.redactions
}
#[must_use]
pub const fn language_id(&self) -> &LanguageId {
&self.config.language_id
}
pub async fn state(&self) -> super::ServerState {
*self.state.lock().await
}
#[must_use]
pub const fn request_timeout(&self) -> Duration {
self.config.request_timeout_seconds.as_duration()
}
#[must_use]
pub fn completion_timeout(&self) -> Duration {
self.request_timeout().min(COMPLETION_TIMEOUT_CAP)
}
#[must_use]
pub fn code_action_resolve_timeout(&self) -> Duration {
self.request_timeout().min(CODE_ACTION_RESOLVE_TIMEOUT_CAP)
}
async fn register_and_send_request(
&self,
request: JsonRpcRequest,
responder: oneshot::Sender<Result<Value>>,
) -> Result<InFlightRequest<'_>> {
let mut in_flight = InFlightRequest::register(self, request.id.clone(), responder);
self.command_tx
.send(ClientCommand::SendRequest { request })
.await
.map_err(|_| Error::ServerTerminated)?;
in_flight.mark_sent();
Ok(in_flight)
}
pub async fn request<P, R>(
&self,
method: &str,
params: P,
timeout_duration: Duration,
) -> Result<R>
where
P: Serialize,
R: DeserializeOwned,
{
Box::pin(self.request_inner(method, params, timeout_duration))
.await
.map_err(UnclassifiedError::surface)
}
async fn request_inner<P, R>(
&self,
method: &str,
params: P,
timeout_duration: Duration,
) -> std::result::Result<R, UnclassifiedError>
where
P: Serialize,
R: DeserializeOwned,
{
let params_value = Self::omit_null_params(
serde_json::to_value(params).map_err(|e| UnclassifiedError::logged(e.into()))?,
);
let mut delay_ms = SERVER_CANCELLED_INITIAL_DELAY_MS;
for attempt in 0..=SERVER_CANCELLED_MAX_RETRIES {
if attempt > 0 {
debug!(
"Retrying {} (attempt {}/{}), backoff={}ms",
method, attempt, SERVER_CANCELLED_MAX_RETRIES, delay_ms
);
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
delay_ms = delay_ms.saturating_mul(2);
}
let id = RequestId::Number(self.request_counter.fetch_add(1, Ordering::SeqCst));
let (response_tx, response_rx) = oneshot::channel();
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: id.clone(),
method: method.to_string(),
params: params_value.clone(),
};
debug!("Sending request: {} (id={:?})", method, id);
let mut in_flight = self
.register_and_send_request(request, response_tx)
.await
.map_err(UnclassifiedError::logged)?;
let outcome = match timeout(timeout_duration, response_rx).await {
Ok(received) => {
in_flight.mark_settled();
received.map_err(|_| UnclassifiedError::logged(Error::ServerTerminated))?
}
Err(_elapsed) => {
in_flight.expire();
return Err(UnclassifiedError::logged(Error::Timeout(timeout_duration)));
}
};
match outcome {
Ok(result_value) => {
return serde_json::from_value(result_value).map_err(|e| {
UnclassifiedError::logged(
self.redactions.protocol_error(format_args!(
"Failed to deserialize response: {e}"
)),
)
});
}
Err(Error::LspServerError(response)) if Self::retries(&response, method) => {
if attempt == SERVER_CANCELLED_MAX_RETRIES {
error!(
"LSP error response: {} (code {}) on '{}' (id={:?}), retries exhausted",
Self::truncate_error_message_for_log(&response.message),
response.code,
method,
id
);
return Err(UnclassifiedError::logged(Error::LspServerError(response)));
}
warn!(
"LSP error response: {} (code {}) on '{}' (id={:?}), will retry",
Self::truncate_error_message_for_log(&response.message),
response.code,
method,
id
);
}
Err(Error::LspServerError(response)) => {
return Err(UnclassifiedError::server_response(response, method, id));
}
Err(e) => return Err(UnclassifiedError::logged(e)),
}
}
Err(UnclassifiedError::logged(Error::ServerTerminated))
}
fn retries(response: &JsonRpcError, method: &str) -> bool {
let retryable = match LspErrorCodes::from(response.code) {
LspErrorCodes::ServerCancelled => true,
LspErrorCodes::ContentModified => {
CONTENT_MODIFIED_RETRY_METHODS.contains(&LspRequestMethod::from(method))
}
_ => false,
};
retryable && Self::should_retrigger(response.data.as_ref())
}
pub(crate) async fn request_typed_classified<R>(
&self,
params: R::Params,
timeout_duration: Duration,
) -> std::result::Result<R::Result, UnclassifiedError>
where
R: lsp_types::Request,
{
Box::pin(self.request_inner(R::METHOD.as_str(), params, timeout_duration)).await
}
pub async fn request_typed<R>(
&self,
params: R::Params,
timeout_duration: Duration,
) -> Result<R::Result>
where
R: lsp_types::Request,
{
self.request(R::METHOD.as_str(), params, timeout_duration)
.await
}
fn should_retrigger(data: Option<&Value>) -> bool {
data.is_none_or(|v| {
v.get("retriggerRequest")
.and_then(Value::as_bool)
.unwrap_or(true)
})
}
pub(crate) fn is_message_loop_finished(&self) -> bool {
self.receiver_task
.as_ref()
.is_some_and(JoinHandle::is_finished)
}
pub(crate) fn fail_pending_requests(&self) {
Self::drain_and_fail_pending(&self.pending_requests, &self.pending_failure);
}
pub(crate) fn mark_restarted(&self, server_id: ServerId) {
*self
.pending_failure
.lock()
.unwrap_or_else(PoisonError::into_inner) = PendingFailure::Restarted(server_id);
}
fn drain_and_fail_pending(pending: &PendingRequests, failure: &StdMutex<PendingFailure>) {
for sender in pending.drain_waiting() {
let error = failure
.lock()
.unwrap_or_else(PoisonError::into_inner)
.error();
let _ = sender.send(Err(error));
}
}
pub async fn notify<P>(&self, method: &str, params: P) -> Result<()>
where
P: Serialize,
{
let notification = OutboundNotification::new(method, params)?;
debug!("Sending notification: {}", method);
self.send_notification(notification).await
}
pub async fn notify_typed<N>(&self, params: N::Params) -> Result<()>
where
N: lsp_types::Notification,
{
self.notify(N::METHOD.as_str(), params).await
}
#[allow(
clippy::arithmetic_side_effects,
reason = "SHUTDOWN_TIMEOUT is a small constant"
)]
pub async fn shutdown(mut self) -> Result<()> {
self.shutdown_until(Instant::now() + SHUTDOWN_TIMEOUT).await
}
pub(crate) async fn shutdown_until(&mut self, deadline: Instant) -> Result<()> {
debug!("Shutting down LSP client");
self.suppress_cancels();
let mut task = self.receiver_task.take();
let command_tx = &self.command_tx;
let outcome = timeout_at(deadline, async {
let _ = command_tx.send(ClientCommand::Shutdown).await;
match task.as_mut() {
Some(task) => match task.await {
Ok(result) => result,
Err(source) => Err(Error::TaskFailed {
task: BackgroundTask::LspReceiver,
source,
}),
},
None => Ok(()),
}
})
.await;
let result = outcome.unwrap_or(Err(Error::ShutdownTimeout));
if matches!(result, Err(Error::ShutdownTimeout)) {
if let Some(task) = task {
task.abort();
let _ = timeout(SHUTDOWN_ABORT_SETTLE, task).await;
}
Self::drain_and_fail_pending(&self.pending_requests, &self.pending_failure);
}
*self.state.lock().await = super::ServerState::Shutdown;
result
}
#[allow(
clippy::too_many_arguments,
reason = "one-to-one with the spawned loop's channels and shared state"
)]
async fn message_loop(
transport: (LspTransport, LspTransportReader),
mut command_rx: mpsc::Receiver<ClientCommand>,
pending_requests: PendingRequests,
pending_failure: Arc<StdMutex<PendingFailure>>,
lanes: Option<Lanes>,
redactions: Arc<Redactions>,
settings: Option<Arc<LspSettings>>,
) -> Result<()> {
debug!("Message loop started");
let (mut transport, reader) = transport;
let (reader_handle, mut msg_rx) = spawn_reader_task(reader);
let result = {
let _abort_reader_on_drop = crate::util::AbortOnDrop(&reader_handle);
Self::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
lanes.as_ref(),
&redactions,
settings.as_deref(),
)
.await
};
let _ = timeout(READER_TASK_ABORT_GRACE, reader_handle).await;
drop(command_rx);
Self::drain_and_fail_pending(&pending_requests, &pending_failure);
if let Err(ref e) = result {
error!("Message loop exiting with error: {}", e);
} else {
debug!("Message loop exiting normally");
}
result
}
fn omit_null_params(params: Value) -> Option<Value> {
if params.is_null() { None } else { Some(params) }
}
fn truncate_error_message_for_log(message: &str) -> String {
crate::util::truncate_str(message, crate::util::MAX_LOG_STRING_BYTES)
}
fn route(notification: LspNotification, lanes: Option<&Lanes>) -> Route<'_> {
let Some(lanes) = lanes else {
return Route::Floor;
};
match notification {
LspNotification::PublishDiagnostics(params) => {
Route::Mailbox(&lanes.notifications, params)
}
LspNotification::LogMessage(params) => {
Route::Messages(&lanes.notifications, ServerMessage::Log(params))
}
LspNotification::ShowMessage(params) => {
Route::Messages(&lanes.notifications, ServerMessage::Show(params))
}
LspNotification::Progress(params)
if crate::lsp::types::ProgressKind::from_value(¶ms.value).is_some() =>
{
Route::Lifecycle(&lanes.lifecycle, LspNotification::Progress(params))
}
LspNotification::Other { method, params }
if method.as_ref() != lsp_types::ProgressNotification::METHOD.as_str() =>
{
Route::Lifecycle(&lanes.lifecycle, LspNotification::Other { method, params })
}
LspNotification::Progress(_) | LspNotification::Other { .. } => Route::Floor,
}
}
fn deliver(route: Route<'_>) -> Delivery {
let delivery = |lane, delivered: bool| {
if delivered {
Delivery::Delivered
} else {
Delivery::Dropped(lane)
}
};
match route {
Route::Mailbox(sink, params) => {
debug!(
"Forwarding diagnostics for {}: {} items",
params.uri.as_ref(),
params.diagnostics.len()
);
sink.publishes().publish(params);
Delivery::Delivered
}
Route::Messages(sink, message) => delivery(
Lane::Notification,
sink.messages().try_send(message).is_ok(),
),
Route::Lifecycle(tx, notification) => {
delivery(Lane::Lifecycle, tx.try_send(notification).is_ok())
}
Route::Floor => Delivery::Delivered,
}
}
#[allow(
clippy::too_many_lines,
clippy::too_many_arguments,
reason = "one select loop over the transport, channels and shared state"
)]
async fn message_loop_inner(
transport: &mut LspTransport,
msg_rx: &mut mpsc::Receiver<Result<InboundMessage>>,
command_rx: &mut mpsc::Receiver<ClientCommand>,
pending_requests: &PendingRequests,
lanes: Option<&Lanes>,
redactions: &Redactions,
settings: Option<&LspSettings>,
) -> Result<()> {
loop {
tokio::select! {
Some(command) = command_rx.recv() => {
match command {
ClientCommand::SendRequest { request } => {
let value = serde_json::to_value(&request)?;
transport.send(&value).await?;
}
ClientCommand::SendNotification { method, params } => {
let notification = serde_json::to_value(JsonRpcNotification {
jsonrpc: JsonRpcVersion,
method,
params,
})?;
transport.send(¬ification).await?;
}
ClientCommand::Shutdown => {
debug!("Client shutdown requested");
while let Ok(message) = msg_rx.try_recv() {
match message {
Ok(m) => {
Self::handle_inbound_message(
transport,
m,
pending_requests,
lanes,
redactions,
settings,
)
.await?;
}
Err(e) => {
error!("Transport receive error while draining on shutdown: {}", e);
break;
}
}
}
break;
}
}
}
message = msg_rx.recv() => {
let message = match message {
Some(Ok(m)) => m,
Some(Err(e)) => {
error!("Transport receive error: {}", e);
return Err(e);
}
None => {
error!("Transport receive error: reader task ended unexpectedly");
return Err(Error::ServerTerminated);
}
};
Self::handle_inbound_message(
transport,
message,
pending_requests,
lanes,
redactions,
settings,
)
.await?;
}
}
}
Ok(())
}
fn complete_pending(
pending_requests: &PendingRequests,
response: JsonRpcResponse,
redactions: &Redactions,
) {
trace!("Received response: id={:?}", response.id);
match pending_requests.resolve(&response.id) {
Resolution::Waiting(sender) => match response.outcome {
JsonRpcOutcome::Error(error) => {
let message = redactions.apply(&error.message);
trace!(
"LSP error response: {} (code {})",
Self::truncate_error_message_for_log(&message),
error.code
);
let caller_message =
crate::util::truncate_str(&message, MAX_ERROR_MESSAGE_CALLER_BYTES);
let _ = sender.send(Err(Error::LspServerError(JsonRpcError {
code: error.code,
message: caller_message,
data: error.data,
})));
}
JsonRpcOutcome::Result(result) => {
let _ = sender.send(Ok(result));
}
},
Resolution::Cancelled(cause) => {
trace!(
"Dropped the reply to a request mcpls cancelled ({cause:?}): id={:?}",
response.id
);
}
Resolution::Unknown => {
warn!(
"Received response for unknown request ID: {:?}",
response.id
);
}
}
}
async fn handle_inbound_message(
transport: &mut LspTransport,
message: InboundMessage,
pending_requests: &PendingRequests,
lanes: Option<&Lanes>,
redactions: &Redactions,
settings: Option<&LspSettings>,
) -> Result<()> {
match message {
InboundMessage::Response(response) => {
Self::complete_pending(pending_requests, response, redactions);
}
InboundMessage::UndecodableResponse { id } => {
warn!("Failed the request answered by an undecodable response: id={id:?}");
if let Resolution::Waiting(sender) = pending_requests.resolve(&id) {
let _ = sender.send(Err(Error::LspProtocolError(RedactedText::fixed(
"undecodable response",
))));
}
}
InboundMessage::UndecodableRequest { id } => {
debug!("Answering an undecodable server request with an error: id={id:?}");
let reply = JsonRpcReply::new(
id,
JsonRpcOutcome::Error(JsonRpcError::new(
ErrorCodes::InvalidRequest,
"Invalid Request: the request could not be decoded",
)),
);
transport.send(&serde_json::to_value(&reply)?).await?;
}
InboundMessage::UndecodableNotification | InboundMessage::UndecodableFrame => {}
InboundMessage::Request(request) => {
debug!(
"Received server request: {} (id={:?})",
request.method, request.id
);
let reply = Self::server_request_reply(request, settings);
let value = serde_json::to_value(&reply)?;
transport.send(&value).await?;
}
InboundMessage::Notification(notification) => {
debug!("Received notification: {}", notification.method);
let mut typed = LspNotification::parse(¬ification.method, notification.params);
Self::redact_notification(&mut typed, redactions);
let route = Self::route(typed, lanes);
if tracing::enabled!(tracing::Level::TRACE) {
let forwarded: Option<&dyn std::fmt::Debug> = match &route {
Route::Messages(_, message) => Some(message),
Route::Lifecycle(_, notification) => Some(notification),
Route::Mailbox(..) | Route::Floor => None,
};
if let Some(payload) = forwarded {
trace!(
"Forwarding notification: {}",
redactions.apply(&format!("{payload:?}"))
);
}
}
if let Delivery::Dropped(lane) = Self::deliver(route) {
DropLog::of(lane).record(¬ification.method);
}
}
}
Ok(())
}
pub(crate) fn redact_notification(notification: &mut LspNotification, redactions: &Redactions) {
if redactions.is_empty() {
return;
}
match notification {
LspNotification::LogMessage(params) => {
redactions.redact_in_place(&mut params.message);
}
LspNotification::ShowMessage(params) => {
redactions.redact_in_place(&mut params.message);
}
LspNotification::PublishDiagnostics(params) => {
for diagnostic in &mut params.diagnostics {
redactions.redact_diagnostic(diagnostic);
}
}
LspNotification::Progress(params) => redactions.redact_progress(&mut params.value),
LspNotification::Other { .. } => {}
}
}
fn server_request_reply(
request: JsonRpcRequest,
settings: Option<&LspSettings>,
) -> JsonRpcReply {
let outcome =
Self::server_request_result(&request.method, request.params.as_ref(), settings);
JsonRpcReply::new(request.id, outcome.into())
}
fn server_request_result(
method: &str,
params: Option<&Value>,
settings: Option<&LspSettings>,
) -> std::result::Result<Value, JsonRpcError> {
match LspRequestMethod::from(method) {
LspRequestMethod::ClientRegisterCapability
| LspRequestMethod::ClientUnregisterCapability
| LspRequestMethod::WorkspaceWorkspaceFolders
| LspRequestMethod::WorkspaceDiagnosticRefresh
| LspRequestMethod::WorkspaceSemanticTokensRefresh
| LspRequestMethod::WorkspaceInlayHintRefresh
| LspRequestMethod::WorkspaceCodeLensRefresh
| LspRequestMethod::WindowShowMessageRequest
| LspRequestMethod::WindowWorkDoneProgressCreate => Ok(Value::Null),
LspRequestMethod::WorkspaceConfiguration => {
Self::workspace_configuration_result(params, settings)
}
LspRequestMethod::WorkspaceApplyEdit => Ok(serde_json::json!({ "applied": false })),
_ => Err(JsonRpcError::new(
ErrorCodes::MethodNotFound,
format!("Unhandled server request: {method}"),
)),
}
}
fn workspace_configuration_result(
params: Option<&Value>,
settings: Option<&LspSettings>,
) -> std::result::Result<Value, JsonRpcError> {
let items = params
.and_then(|params| params.get("items"))
.and_then(Value::as_array);
let Some(settings) = settings else {
return Ok(Value::Array(vec![Value::Null; items.map_or(0, Vec::len)]));
};
let items = items.ok_or_else(|| {
JsonRpcError::new(
ErrorCodes::InvalidParams,
"Invalid params for workspace/configuration",
)
})?;
let sections = items
.iter()
.map(|item| match ConfigurationItem::parse(item) {
ConfigurationItem::Whole => settings.section(None),
ConfigurationItem::Section(section) => settings.section(Some(section)),
ConfigurationItem::Uninterpretable => Value::Null,
})
.collect();
Ok(Value::Array(sections))
}
}
fn log_surfaced_error(method: &str, id: &RequestId, code: i32, message: &str) {
error!(
"LSP error response: {} (code {}) on '{}' (id={:?})",
LspClient::truncate_error_message_for_log(message),
code,
method,
id
);
}
#[derive(Debug)]
#[must_use = "call `surface()` or `handled()` to log and unwrap the error"]
pub struct UnclassifiedError(Box<Unclassified>);
#[derive(Debug)]
enum Unclassified {
Logged(Error),
ServerResponse {
error: JsonRpcError,
method: String,
id: RequestId,
},
}
impl Unclassified {
fn finish(self, report: impl FnOnce(&str, &RequestId, i32, &str)) -> Error {
match self {
Self::Logged(error) => error,
Self::ServerResponse { error, method, id } => {
report(&method, &id, error.code, &error.message);
Error::LspServerError(error)
}
}
}
}
fn log_handled_error(method: &str, id: &RequestId, code: i32, message: &str) {
debug!(
"LSP error response handled by the caller: {} (code {}) on '{}' (id={:?})",
LspClient::truncate_error_message_for_log(message),
code,
method,
id
);
}
impl UnclassifiedError {
fn logged(error: Error) -> Self {
Self(Box::new(Unclassified::Logged(error)))
}
fn server_response(response: JsonRpcError, method: &str, id: RequestId) -> Self {
Self(Box::new(Unclassified::ServerResponse {
error: response,
method: method.to_owned(),
id,
}))
}
fn take(mut self) -> Unclassified {
std::mem::replace(&mut *self.0, Unclassified::Logged(Error::ServerTerminated))
}
pub(crate) fn server_error(&self) -> Option<&JsonRpcError> {
match &*self.0 {
Unclassified::Logged(Error::LspServerError(response))
| Unclassified::ServerResponse {
error: response, ..
} => Some(response),
Unclassified::Logged(_) => None,
}
}
pub(crate) fn timed_out(&self) -> bool {
matches!(&*self.0, Unclassified::Logged(Error::Timeout(_)))
}
pub(crate) fn surface(self) -> Error {
self.take().finish(log_surfaced_error)
}
pub(crate) fn handled(self) -> Error {
self.take().finish(log_handled_error)
}
}
impl Drop for UnclassifiedError {
fn drop(&mut self) {
let inner = std::mem::replace(&mut *self.0, Unclassified::Logged(Error::ServerTerminated));
drop(inner.finish(log_surfaced_error));
}
}
enum ConfigurationItem<'a> {
Whole,
Section(&'a str),
Uninterpretable,
}
impl<'a> ConfigurationItem<'a> {
fn parse(item: &'a Value) -> Self {
let Value::Object(object) = item else {
return Self::Uninterpretable;
};
match object.get("section") {
None | Some(Value::Null) => Self::Whole,
Some(Value::String(section)) => Self::Section(section),
Some(_) => Self::Uninterpretable,
}
}
}
#[cfg(test)]
mod tests {
use std::assert_matches;
use super::*;
use crate::config::TimeoutSecs;
use crate::lsp::types::JsonRpcResponse;
#[test]
fn test_unclassified_server_response_always_reports_code_and_message() {
let unclassified = UnclassifiedError::server_response(
JsonRpcError {
code: -32001,
message: "boom".to_owned(),
data: None,
},
"textDocument/hover",
RequestId::Number(3),
);
let mut reported = None;
let error = unclassified.take().finish(|method, id, code, message| {
reported = Some((method.to_owned(), id.clone(), code, message.to_owned()));
});
assert_eq!(
reported,
Some((
"textDocument/hover".to_owned(),
RequestId::Number(3),
-32001,
"boom".to_owned()
))
);
assert_matches!(
error,
Error::LspServerError(JsonRpcError { code: -32001, .. })
);
}
#[tokio::test]
async fn test_forwarded_notification_trace_is_redacted() {
use tracing_subscriber::prelude::*;
use crate::test_lsp::CapturedLogs;
let redactions =
Redactions::new([("API_TOKEN".to_owned(), "SuperSecretValue123".to_owned())]);
let logs = CapturedLogs::default();
let _guard = tracing::subscriber::set_default(
tracing_subscriber::registry()
.with(tracing_subscriber::filter::LevelFilter::TRACE)
.with(logs.clone()),
);
let (mut transport, _reader) = LspTransport::new(tokio::io::sink(), tokio::io::empty());
let (lanes, mut lifecycle_rx) = open_lanes();
let pending = PendingRequests::default();
let notification = InboundMessage::Notification(JsonRpcNotification {
jsonrpc: JsonRpcVersion,
method: "custom/echo".to_string(),
params: Some(serde_json::json!({"env": "SuperSecretValue123"})),
});
LspClient::handle_inbound_message(
&mut transport,
notification,
&pending,
Some(&lanes),
&redactions,
None,
)
.await
.unwrap();
assert!(lifecycle_rx.recv().await.is_some());
let output = logs.messages().join("\n");
assert!(output.contains("Forwarding notification"), "{output}");
assert!(!output.contains("SuperSecretValue123"), "{output}");
}
fn secret_diagnostics_params() -> Value {
let range = serde_json::json!({
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 1}
});
serde_json::json!({
"uri": "file:///w/main.rs",
"diagnostics": [{
"range": range,
"message": "leak SuperSecretValue123",
"data": {"hint": ["SuperSecretValue123"]}
}]
})
}
#[tokio::test]
async fn test_reader_task_forwards_pushed_diagnostics_redacted() {
use crate::test_lsp::{fake_lsp_client_with_redactions, write_notification};
let redactions =
Redactions::new([("API_TOKEN".to_owned(), "SuperSecretValue123".to_owned())]);
let (_client, mut server, mut lanes) = fake_lsp_client_with_redactions(redactions);
write_notification(
&mut server.read_half_stdin,
"textDocument/publishDiagnostics",
secret_diagnostics_params(),
)
.await;
let forwarded = timeout(Duration::from_secs(2), lanes.publishes.recv())
.await
.unwrap()
.unwrap();
let shown = format!("{forwarded:?}");
assert!(shown.contains("[redacted:API_TOKEN]"), "{shown}");
assert!(!shown.contains("SuperSecretValue123"), "{shown}");
}
#[test]
fn test_redact_notification_leaves_text_alone_without_secrets() {
let mut notification = LspNotification::parse(
"textDocument/publishDiagnostics",
Some(secret_diagnostics_params()),
);
let before = format!("{notification:?}");
LspClient::redact_notification(&mut notification, &Redactions::default());
assert_eq!(format!("{notification:?}"), before);
let unrelated = Redactions::new([("API_TOKEN".to_owned(), "NotInTheText99".to_owned())]);
LspClient::redact_notification(&mut notification, &unrelated);
assert_eq!(format!("{notification:?}"), before);
let matching =
Redactions::new([("API_TOKEN".to_owned(), "SuperSecretValue123".to_owned())]);
LspClient::redact_notification(&mut notification, &matching);
assert!(!format!("{notification:?}").contains("SuperSecretValue123"));
}
#[test]
fn test_request_id_generation() {
let counter = AtomicI64::new(1);
let id1 = counter.fetch_add(1, Ordering::SeqCst);
let id2 = counter.fetch_add(1, Ordering::SeqCst);
let id3 = counter.fetch_add(1, Ordering::SeqCst);
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert_eq!(id3, 3);
}
#[test]
fn test_client_creation() {
let config = LspServerConfig::rust_analyzer();
let client = LspClient::new(config);
assert_eq!(client.language_id(), "rust");
}
#[test]
fn test_client_clone() {
let config = LspServerConfig::rust_analyzer();
let client = LspClient::new(config);
#[allow(clippy::redundant_clone, reason = "the test exercises `Clone` itself")]
let cloned = client.clone();
assert_eq!(cloned.language_id(), "rust");
assert!(
cloned.receiver_task.is_none(),
"Cloned client should not own receiver task"
);
}
#[test]
fn test_request_timeout_and_completion_timeout_at_default() {
let config = LspServerConfig::rust_analyzer();
let client = LspClient::new(config);
assert_eq!(client.request_timeout(), Duration::from_secs(30));
assert_eq!(client.completion_timeout(), Duration::from_secs(10));
}
#[test]
fn test_completion_timeout_clamps_to_ten_seconds() {
for secs in [1, 2, 3, 30, 300] {
let mut config = LspServerConfig::rust_analyzer();
config.request_timeout_seconds = TimeoutSecs::new(secs).unwrap();
let client = LspClient::new(config);
assert_eq!(
client.completion_timeout(),
Duration::from_secs(secs.min(10)),
"request_timeout_seconds={secs}"
);
assert!(client.completion_timeout() <= client.request_timeout());
}
}
#[test]
fn test_code_action_resolve_timeout_clamps_to_ten_seconds() {
for secs in [1, 2, 3, 30, 300] {
let mut config = LspServerConfig::rust_analyzer();
config.request_timeout_seconds = TimeoutSecs::new(secs).unwrap();
let client = LspClient::new(config);
assert_eq!(
client.code_action_resolve_timeout(),
Duration::from_secs(secs.min(10)),
"request_timeout_seconds={secs}"
);
assert!(client.code_action_resolve_timeout() <= client.request_timeout());
}
}
#[test]
fn test_request_timeout_independent_per_server() {
let mut config_a = LspServerConfig::rust_analyzer();
config_a.request_timeout_seconds = TimeoutSecs::new(5).unwrap();
let mut config_b = LspServerConfig::pyright();
config_b.request_timeout_seconds = TimeoutSecs::new(15).unwrap();
let client_a = LspClient::new(config_a);
let client_b = LspClient::new(config_b);
assert_eq!(client_a.request_timeout(), Duration::from_secs(5));
assert_eq!(client_b.request_timeout(), Duration::from_secs(15));
}
#[test]
fn test_register_capability_request_is_acknowledged() {
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: RequestId::String("ts1".to_string()),
method: "client/registerCapability".to_string(),
params: Some(serde_json::json!({ "registrations": [] })),
};
let reply = reply_json(request);
assert_eq!(reply["id"], serde_json::json!("ts1"));
assert_eq!(reply["jsonrpc"], serde_json::json!("2.0"));
assert_eq!(reply["result"], Value::Null);
assert!(reply.get("error").is_none());
}
#[test]
fn test_workspace_configuration_request_returns_null_per_item_without_settings() {
let result = LspClient::workspace_configuration_result(
Some(&serde_json::json!({
"items": [{ "section": "typescript" }, { "section": "editor" }]
})),
None,
);
assert_eq!(result.unwrap(), serde_json::json!([null, null]));
}
fn sample_settings() -> LspSettings {
serde_json::from_value(serde_json::json!({
"python.analysis.typeCheckingMode": "strict",
"gopls": { "ui.semanticTokens": true }
}))
.unwrap()
}
#[test]
fn test_workspace_configuration_serves_settings_sections() {
let settings = sample_settings();
let result = LspClient::workspace_configuration_result(
Some(&serde_json::json!({
"items": [
{ "section": "python.analysis" },
{ "section": "gopls", "scopeUri": "file:///ignored" },
{ "section": "missing" },
{}
]
})),
Some(&settings),
)
.unwrap();
assert_eq!(
result,
serde_json::json!([
{ "typeCheckingMode": "strict" },
{ "ui.semanticTokens": true },
null,
settings.to_value()
])
);
}
#[test]
fn test_workspace_configuration_non_object_item_gets_null() {
let settings = sample_settings();
let result = LspClient::workspace_configuration_result(
Some(&serde_json::json!({
"items": [
{ "section": "python.analysis" },
{ "section": "missing.x" },
{},
{ "section": 5 },
"x",
null,
7
]
})),
Some(&settings),
)
.unwrap();
assert_eq!(
result,
serde_json::json!([
{ "typeCheckingMode": "strict" },
null,
settings.to_value(),
null,
null,
null,
null
])
);
}
#[tokio::test]
async fn test_message_loop_answers_workspace_configuration_from_settings() {
use tokio::io::BufReader;
use crate::test_lsp::{fake_lsp_client_with_config, read_framed_message, write_request};
let mut config = LspServerConfig::rust_analyzer();
config.settings = Some(sample_settings());
let (_client, mut server) = fake_lsp_client_with_config(config);
write_request(
&mut server.read_half_stdin,
&serde_json::json!(7),
"workspace/configuration",
serde_json::json!({ "items": [{ "section": "python.analysis", "scopeUri": 1 }] }),
)
.await;
let mut reader = BufReader::new(&mut server.write_stdout);
let response = read_framed_message(&mut reader).await;
assert_eq!(response["id"], 7);
assert_eq!(
response["result"],
serde_json::json!([{ "typeCheckingMode": "strict" }])
);
}
#[test]
fn test_workspace_configuration_is_lenient_about_item_shape() {
let settings = sample_settings();
let params = serde_json::json!({
"items": [
{ "section": 5 },
{ "section": null },
3,
{ "section": "gopls", "scopeUri": 42 }
]
});
let with = LspClient::workspace_configuration_result(Some(¶ms), Some(&settings));
let without = LspClient::workspace_configuration_result(Some(¶ms), None);
assert_eq!(
with.unwrap(),
serde_json::json!([
null,
settings.to_value(),
null,
{ "ui.semanticTokens": true }
])
);
assert_eq!(
without.unwrap(),
serde_json::json!([null, null, null, null])
);
}
#[test]
fn test_workspace_configuration_without_settings_matches_the_old_reply_for_bad_params() {
for params in [
None,
Some(serde_json::json!({})),
Some(serde_json::json!({ "items": "x" })),
] {
let result = LspClient::workspace_configuration_result(params.as_ref(), None);
assert_eq!(result.unwrap(), serde_json::json!([]));
}
}
#[test]
fn test_workspace_configuration_with_settings_rejects_missing_items() {
for params in [None, Some(serde_json::json!({ "items": "nope" }))] {
let error = LspClient::workspace_configuration_result(
params.as_ref(),
Some(&sample_settings()),
)
.unwrap_err();
assert_eq!(error.code, -32602);
}
}
#[test]
fn test_work_done_progress_create_request_is_acknowledged() {
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: RequestId::String("wdp1".to_string()),
method: "window/workDoneProgress/create".to_string(),
params: Some(serde_json::json!({ "token": "indexing" })),
};
let reply = reply_json(request);
assert_eq!(reply["result"], Value::Null);
assert!(reply.get("error").is_none());
}
fn progress(value: serde_json::Value) -> LspNotification {
LspNotification::Progress(lsp_types::ProgressParams {
token: lsp_types::ProgressToken::Int(1),
value,
})
}
fn other(method: &'static str) -> LspNotification {
LspNotification::Other {
method: std::borrow::Cow::Borrowed(method),
params: None,
}
}
fn open_lanes() -> (Lanes, mpsc::Receiver<LspNotification>) {
let (messages_tx, _messages_rx) = mpsc::channel(1);
let (notifications, _reader) = NotificationSink::new(messages_tx);
let (lifecycle, lifecycle_rx) = mpsc::channel(4);
(
Lanes {
notifications,
lifecycle,
},
lifecycle_rx,
)
}
#[test]
fn test_report_progress_frame_goes_to_the_floor() {
let report = progress(serde_json::json!({ "kind": "report", "percentage": 50 }));
let (lanes, _rx) = open_lanes();
assert_matches!(LspClient::route(report, Some(&lanes)), Route::Floor);
}
#[test]
fn test_malformed_progress_other_goes_to_the_floor() {
let (lanes, _rx) = open_lanes();
assert_matches!(
LspClient::route(other("$/progress"), Some(&lanes)),
Route::Floor
);
}
#[test]
fn test_begin_and_other_notifications_reach_lifecycle_lane() {
let begin = progress(serde_json::json!({ "kind": "begin", "title": "Indexing" }));
let end = progress(serde_json::json!({ "kind": "end" }));
let (lanes, _rx) = open_lanes();
for notification in [begin, end, other("experimental/serverStatus")] {
assert_matches!(
LspClient::route(notification, Some(&lanes)),
Route::Lifecycle(..)
);
}
}
#[test]
fn test_route_covers_every_notification_shape() {
let publish = || {
LspNotification::PublishDiagnostics(PublishDiagnosticsParams {
uri: lsp_types::Uri::from("file:///a.rs".to_owned()),
diagnostics: Vec::new(),
version: None,
})
};
let log = || {
LspNotification::LogMessage(lsp_types::LogMessageParams {
kind: lsp_types::MessageType::Info,
message: "m".to_owned(),
})
};
let show = || {
LspNotification::ShowMessage(lsp_types::ShowMessageParams {
kind: lsp_types::MessageType::Info,
message: "m".to_owned(),
})
};
let (lanes, _rx) = open_lanes();
assert_matches!(
LspClient::route(publish(), Some(&lanes)),
Route::Mailbox(..)
);
assert_matches!(
LspClient::route(log(), Some(&lanes)),
Route::Messages(_, ServerMessage::Log(_))
);
assert_matches!(
LspClient::route(show(), Some(&lanes)),
Route::Messages(_, ServerMessage::Show(_))
);
for notification in [
progress(serde_json::json!({ "kind": "begin" })),
other("experimental/serverStatus"),
] {
assert_matches!(
LspClient::route(notification, Some(&lanes)),
Route::Lifecycle(..)
);
}
for notification in [publish(), log(), show(), other("custom/x")] {
assert_matches!(LspClient::route(notification, None), Route::Floor);
}
}
#[test]
fn test_deliver_reports_the_lane_that_dropped_a_frame() {
let (lifecycle_tx, lifecycle_rx) = mpsc::channel(1);
drop(lifecycle_rx);
let delivery = LspClient::deliver(Route::Lifecycle(
&lifecycle_tx,
other("experimental/serverStatus"),
));
assert_eq!(delivery, Delivery::Dropped(Lane::Lifecycle));
assert_eq!(LspClient::deliver(Route::Floor), Delivery::Delivered);
}
#[test]
fn test_unknown_server_request_returns_method_not_found() {
let request = JsonRpcRequest {
jsonrpc: JsonRpcVersion,
id: RequestId::String("unknown-1".to_string()),
method: "custom/request".to_string(),
params: None,
};
let reply = reply_json(request);
assert!(reply.get("result").is_none());
assert_eq!(reply["error"]["code"], serde_json::json!(-32601));
assert_eq!(
reply["error"]["message"],
serde_json::json!("Unhandled server request: custom/request")
);
}
fn reply_json(request: JsonRpcRequest) -> Value {
serde_json::to_value(LspClient::server_request_reply(request, None)).unwrap()
}
#[tokio::test]
async fn test_null_response_handling() {
use crate::lsp::types::{JsonRpcResponse, RequestId};
let pending_requests = PendingRequests::default();
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests.register(RequestId::Number(1), response_tx);
let null_response = JsonRpcResponse {
jsonrpc: JsonRpcVersion,
id: RequestId::Number(1),
outcome: JsonRpcOutcome::Result(Value::Null),
};
let Resolution::Waiting(sender) = pending_requests.resolve(&null_response.id) else {
panic!("the request must be waiting");
};
let _ = sender.send(Ok(Value::Null));
let timeout_result =
tokio::time::timeout(tokio::time::Duration::from_millis(100), response_rx).await;
assert!(timeout_result.is_ok(), "Should not timeout");
let channel_result = timeout_result.unwrap();
assert!(
channel_result.is_ok(),
"Channel should not be closed: {:?}",
channel_result.err()
);
let response = channel_result.unwrap();
assert!(
response.is_ok(),
"Should receive Ok(Value::Null), not Err: {:?}",
response.err()
);
let value = response.unwrap();
assert_eq!(value, Value::Null, "Should receive Value::Null");
}
#[tokio::test]
async fn test_error_response_handling() {
use crate::lsp::types::{JsonRpcError, JsonRpcResponse, RequestId};
let pending_requests = PendingRequests::default();
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests.register(RequestId::Number(1), response_tx);
let error_response = JsonRpcResponse {
jsonrpc: JsonRpcVersion,
id: RequestId::Number(1),
outcome: JsonRpcOutcome::Error(JsonRpcError {
code: -32601,
message: "Method not found".to_string(),
data: None,
}),
};
if let Resolution::Waiting(sender) = pending_requests.resolve(&error_response.id)
&& let JsonRpcOutcome::Error(error) = error_response.outcome
{
let _ = sender.send(Err(Error::LspServerError(error)));
}
let result = response_rx.await.unwrap();
assert!(result.is_err(), "Should receive error");
if let Err(Error::LspServerError(JsonRpcError { code, message, .. })) = result {
assert_eq!(code, -32601);
assert_eq!(message, "Method not found");
} else {
panic!("Expected LspServerError");
}
}
#[tokio::test]
async fn test_unknown_request_id() {
use crate::lsp::types::{JsonRpcResponse, RequestId};
let pending_requests = PendingRequests::default();
let response = JsonRpcResponse {
jsonrpc: JsonRpcVersion,
id: RequestId::Number(999),
outcome: JsonRpcOutcome::Result(Value::Null),
};
assert_matches!(
pending_requests.resolve(&response.id),
Resolution::Unknown,
"Should not find sender for unknown ID"
);
}
#[test]
fn test_truncate_error_message_for_log_handles_multibyte_boundary() {
let message = format!("{}€{}", "x".repeat(199), "y".repeat(50));
let truncated = LspClient::truncate_error_message_for_log(&message);
assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(199)));
}
#[test]
fn test_truncate_error_message_for_log_no_truncation_at_or_below_limit() {
let exact = "x".repeat(200);
assert_eq!(LspClient::truncate_error_message_for_log(&exact), exact);
assert_eq!(LspClient::truncate_error_message_for_log(""), "");
}
#[test]
fn test_truncate_error_message_for_log_truncates_just_above_limit() {
let message = "x".repeat(201);
assert_eq!(
LspClient::truncate_error_message_for_log(&message),
format!("{}... (truncated)", "x".repeat(200))
);
}
#[test]
fn test_truncate_error_message_for_log_handles_wide_char_at_limit() {
let message = format!("{}{}", "x".repeat(197), "🦀".repeat(10));
let truncated = LspClient::truncate_error_message_for_log(&message);
assert_eq!(truncated, format!("{}... (truncated)", "x".repeat(197)));
}
#[tokio::test]
async fn test_concurrent_request_ids() {
let counter = Arc::new(AtomicI64::new(1));
let counter1 = Arc::clone(&counter);
let counter2 = Arc::clone(&counter);
let counter3 = Arc::clone(&counter);
let handles = vec![
tokio::spawn(async move { counter1.fetch_add(1, Ordering::SeqCst) }),
tokio::spawn(async move { counter2.fetch_add(1, Ordering::SeqCst) }),
tokio::spawn(async move { counter3.fetch_add(1, Ordering::SeqCst) }),
];
let mut ids = Vec::new();
for handle in handles {
ids.push(handle.await.unwrap());
}
ids.sort_unstable();
assert_eq!(ids, vec![1, 2, 3], "IDs should be unique and sequential");
}
#[test]
fn test_jsonrpc_version_constant() {
assert_eq!(
serde_json::to_value(JsonRpcVersion).unwrap(),
serde_json::json!("2.0")
);
}
#[cfg(unix)]
#[tokio::test]
async fn test_request_timeout_removes_pending_entry() {
let mut child = tokio::process::Command::new("sleep")
.arg("2")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.kill_on_drop(true)
.spawn()
.unwrap();
let stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let transport = LspTransport::new(stdin, stdout);
let client = LspClient::from_transport(LspServerConfig::rust_analyzer(), transport);
let result: Result<Value> = client
.request(
"textDocument/hover",
serde_json::json!({}),
Duration::from_millis(50),
)
.await;
assert_matches!(result, Err(Error::Timeout(_)), "got {result:?}");
assert!(
client.pending_requests.no_waiting(),
"timed-out request must not remain in pending_requests"
);
}
#[tokio::test]
async fn test_fail_pending_requests_resolves_all_as_server_terminated() {
let pending_requests = PendingRequests::default();
let (command_tx, _command_rx) = mpsc::channel(1);
let client = LspClient {
config: LspServerConfig::rust_analyzer(),
state: Arc::new(Mutex::new(super::super::ServerState::Ready)),
request_counter: Arc::new(AtomicI64::new(1)),
command_tx,
pending_requests: pending_requests.clone(),
closing: Arc::default(),
pending_failure: Arc::default(),
receiver_task: None,
redactions: Arc::default(),
connection_id: ConnectionId::next(),
};
let (tx1, rx1) = oneshot::channel::<Result<Value>>();
let (tx2, rx2) = oneshot::channel::<Result<Value>>();
pending_requests.register(RequestId::Number(1), tx1);
pending_requests.register(RequestId::Number(2), tx2);
client.fail_pending_requests();
assert!(pending_requests.no_waiting());
assert_matches!(rx1.await.unwrap(), Err(Error::ServerTerminated));
assert_matches!(rx2.await.unwrap(), Err(Error::ServerTerminated));
}
mod timeout_cancellation {
use tokio::io::BufReader;
use tokio::task::JoinHandle;
use super::*;
use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
const TIMEOUT: Duration = Duration::from_secs(5);
fn spawn_hover(client: &LspClient) -> JoinHandle<Result<Value>> {
let client = client.clone();
tokio::spawn(async move {
client
.request("textDocument/hover", serde_json::json!({}), TIMEOUT)
.await
})
}
#[tokio::test(start_paused = true)]
async fn test_timeout_while_awaiting_reply_clears_pending_and_discards_late_reply() {
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let first = spawn_hover(&client);
let first_request = read_framed_message(&mut reader).await;
tokio::time::advance(TIMEOUT + Duration::from_secs(1)).await;
assert_matches!(
first.await.unwrap(),
Err(Error::Timeout(limit)) if limit == TIMEOUT,
"request awaiting its reply must time out"
);
assert!(
client.pending_requests.no_waiting(),
"timed-out request must not remain in pending_requests"
);
let cancel = read_framed_message(&mut reader).await;
assert_eq!(cancel["method"], "$/cancelRequest");
assert_eq!(cancel["params"]["id"], first_request["id"]);
write_response(
&mut server.read_half_stdin,
&first_request["id"],
serde_json::json!("stale"),
)
.await;
let second = spawn_hover(&client);
let second_request = read_framed_message(&mut reader).await;
assert_ne!(second_request["id"], first_request["id"]);
write_response(
&mut server.read_half_stdin,
&second_request["id"],
serde_json::json!("fresh"),
)
.await;
assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("fresh"));
assert!(client.pending_requests.no_waiting());
}
#[tokio::test]
async fn test_undecodable_response_fails_only_its_request() {
use crate::test_lsp::write_raw_frame;
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let first = spawn_hover(&client);
let first_request = read_framed_message(&mut reader).await;
let deep = format!("{}1{}", "{\"parent\":".repeat(200), "}".repeat(200));
write_raw_frame(
&mut server.read_half_stdin,
&format!(
r#"{{"jsonrpc":"2.0","id":{},"result":{deep}}}"#,
first_request["id"]
),
)
.await;
assert_matches!(
first.await.unwrap(),
Err(Error::LspProtocolError(message)) if message.as_str() == "undecodable response"
);
let second = spawn_hover(&client);
let second_request = read_framed_message(&mut reader).await;
write_response(
&mut server.read_half_stdin,
&second_request["id"],
serde_json::json!("alive"),
)
.await;
assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("alive"));
assert!(client.pending_requests.no_waiting());
}
fn deeply_nested() -> String {
format!("{}1{}", "{\"parent\":".repeat(200), "}".repeat(200))
}
#[tokio::test]
async fn test_invalid_utf8_response_fails_only_its_request() {
use tokio::io::AsyncWriteExt as _;
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let first = spawn_hover(&client);
let first_request = read_framed_message(&mut reader).await;
let mut body = format!(
r#"{{"jsonrpc":"2.0","id":{},"result":"x"#,
first_request["id"]
)
.into_bytes();
body.extend_from_slice(&[0xff, 0xfe]);
body.extend_from_slice(br#"y"}"#);
let stdin = &mut server.read_half_stdin;
stdin
.write_all(format!("Content-Length: {}\r\n\r\n", body.len()).as_bytes())
.await
.unwrap();
stdin.write_all(&body).await.unwrap();
stdin.flush().await.unwrap();
assert_matches!(
first.await.unwrap(),
Err(Error::LspProtocolError(message)) if message.as_str() == "undecodable response"
);
let second = spawn_hover(&client);
let second_request = read_framed_message(&mut reader).await;
write_response(
&mut server.read_half_stdin,
&second_request["id"],
serde_json::json!("alive"),
)
.await;
assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("alive"));
}
#[test]
fn test_undecodable_warning_is_time_limited_and_counts_the_suppressed() {
use tracing_subscriber::layer::SubscriberExt as _;
let captured = crate::test_lsp::CapturedLogs::default();
let _guard = tracing::subscriber::set_default(
tracing_subscriber::registry().with(captured.clone()),
);
let mut run = UndecodableRun::default();
let start = std::time::Instant::now();
let at = |secs| start + Duration::from_secs(secs);
for secs in [0, 1, 2, 30] {
run.observe_at(InboundMessage::UndecodableFrame, at(secs))
.unwrap();
}
run.observe_at(InboundMessage::UndecodableFrame, at(61))
.unwrap();
let warnings: Vec<_> = captured
.entries()
.into_iter()
.filter(|(level, _)| *level == tracing::Level::WARN)
.map(|(_, message)| message)
.collect();
assert_eq!(warnings.len(), 2, "{warnings:?}");
assert!(warnings[0].contains("(0 others"), "{warnings:?}");
assert!(warnings[1].contains("(3 others"), "{warnings:?}");
}
#[tokio::test]
async fn test_undecodable_server_request_is_answered_with_an_error() {
use crate::test_lsp::write_raw_frame;
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
write_raw_frame(
&mut server.read_half_stdin,
&format!(
r#"{{"jsonrpc":"2.0","id":"srv-1","method":"client/registerCapability","params":{}}}"#,
deeply_nested()
),
)
.await;
let reply = read_framed_message(&mut reader).await;
assert_eq!(reply["id"], serde_json::json!("srv-1"));
assert_eq!(reply["error"]["code"], serde_json::json!(-32600));
let hover = spawn_hover(&client);
let request = read_framed_message(&mut reader).await;
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!("alive"),
)
.await;
assert_eq!(hover.await.unwrap().unwrap(), serde_json::json!("alive"));
}
#[tokio::test]
async fn test_undecodable_notification_is_dropped_and_the_connection_stays_up() {
use crate::test_lsp::write_raw_frame;
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let hover = spawn_hover(&client);
let request = read_framed_message(&mut reader).await;
write_raw_frame(
&mut server.read_half_stdin,
&format!(
r#"{{"jsonrpc":"2.0","method":"window/logMessage","params":{}}}"#,
deeply_nested()
),
)
.await;
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!("alive"),
)
.await;
assert_eq!(hover.await.unwrap().unwrap(), serde_json::json!("alive"));
}
#[tokio::test]
async fn test_a_run_of_undecodable_frames_ends_the_connection() {
use crate::test_lsp::write_raw_frame;
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let hover = spawn_hover(&client);
let _request = read_framed_message(&mut reader).await;
for _ in 0..MAX_CONSECUTIVE_UNDECODABLE_FRAMES {
write_raw_frame(&mut server.read_half_stdin, "{not json").await;
}
assert_matches!(hover.await.unwrap(), Err(Error::ServerTerminated));
}
#[tokio::test]
async fn test_a_decodable_message_ends_a_run_of_undecodable_frames() {
use crate::test_lsp::write_raw_frame;
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let hover = spawn_hover(&client);
let request = read_framed_message(&mut reader).await;
let almost = MAX_CONSECUTIVE_UNDECODABLE_FRAMES - 1;
for _ in 0..2 {
for _ in 0..almost {
write_raw_frame(&mut server.read_half_stdin, "{not json").await;
}
write_raw_frame(
&mut server.read_half_stdin,
r#"{"jsonrpc":"2.0","method":"$/progress"}"#,
)
.await;
}
write_response(
&mut server.read_half_stdin,
&request["id"],
serde_json::json!("alive"),
)
.await;
assert_eq!(hover.await.unwrap().unwrap(), serde_json::json!("alive"));
}
#[tokio::test(start_paused = true)]
async fn test_dropped_caller_after_send_is_cleared_by_late_reply() {
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let first = spawn_hover(&client);
let first_request = read_framed_message(&mut reader).await;
first.abort();
assert!(first.await.unwrap_err().is_cancelled());
let cancel = read_framed_message(&mut reader).await;
assert_eq!(cancel["method"], "$/cancelRequest");
assert_eq!(cancel["params"]["id"], first_request["id"]);
write_response(
&mut server.read_half_stdin,
&first_request["id"],
serde_json::json!("orphaned"),
)
.await;
let second = spawn_hover(&client);
let second_request = read_framed_message(&mut reader).await;
write_response(
&mut server.read_half_stdin,
&second_request["id"],
serde_json::json!("fresh"),
)
.await;
assert_eq!(second.await.unwrap().unwrap(), serde_json::json!("fresh"));
assert!(
client.pending_requests.no_waiting(),
"the orphaned reply must clear the abandoned entry"
);
}
}
#[test]
fn test_should_retrigger_defaults_to_true_when_data_absent() {
assert!(LspClient::should_retrigger(None));
}
#[test]
fn test_should_retrigger_false_when_flag_false() {
assert!(!LspClient::should_retrigger(Some(&serde_json::json!({
"retriggerRequest": false
}))));
}
#[test]
fn test_should_retrigger_true_when_flag_true() {
assert!(LspClient::should_retrigger(Some(&serde_json::json!({
"retriggerRequest": true
}))));
}
mod void_params_wire {
use tokio::io::BufReader;
use super::*;
use crate::test_lsp::{fake_lsp_client, read_framed_message, write_response};
#[tokio::test]
async fn test_request_with_null_params_omits_params_key() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>("shutdown", Value::Null, Duration::from_secs(5))
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
assert_eq!(request["method"], "shutdown");
assert!(
request.get("params").is_none(),
"null params must be omitted, got: {request}"
);
write_response(&mut server.read_half_stdin, &request["id"], Value::Null).await;
request_task.await.unwrap().unwrap();
}
#[tokio::test]
async fn test_notify_with_null_params_omits_params_key() {
let (client, mut server) = fake_lsp_client();
client.notify("exit", Value::Null).await.unwrap();
let mut reader = BufReader::new(&mut server.write_stdout);
let notification = read_framed_message(&mut reader).await;
assert_eq!(notification["method"], "exit");
assert!(
notification.get("params").is_none(),
"null params must be omitted, got: {notification}"
);
}
#[tokio::test]
async fn test_notify_with_empty_object_params_keeps_params_key() {
let (client, mut server) = fake_lsp_client();
client
.notify("initialized", lsp_types::InitializedParams {})
.await
.unwrap();
let mut reader = BufReader::new(&mut server.write_stdout);
let notification = read_framed_message(&mut reader).await;
assert_eq!(notification["method"], "initialized");
assert_eq!(
notification["params"],
serde_json::json!({}),
"non-null params must still be sent"
);
}
}
mod retry_behavior {
use tokio::io::{AsyncWriteExt, BufReader, DuplexStream};
use super::*;
use crate::test_lsp::{
CapturedLogs, fake_lsp_client, read_framed_message, write_error_response,
write_response as write_success_response,
};
async fn write_retryable_error_response(
stdin: &mut DuplexStream,
id: &Value,
code: i32,
message: &str,
retrigger: bool,
) {
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"error": {
"code": code,
"message": message,
"data": { "retriggerRequest": retrigger },
},
});
let content = serde_json::to_string(&response).unwrap();
let header = format!("Content-Length: {}\r\n\r\n", content.len());
stdin.write_all(header.as_bytes()).await.unwrap();
stdin.write_all(content.as_bytes()).await.unwrap();
stdin.flush().await.unwrap();
}
#[tokio::test]
async fn test_retry_exhaustion_returns_original_server_cancelled_error() {
use tracing_subscriber::layer::SubscriberExt as _;
let (client, mut server) = fake_lsp_client();
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ServerCancelled),
"server cancelled the request",
true,
)
.await;
}
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError {
code,
message,
data,
})) => {
assert_eq!(code, i32::from(LspErrorCodes::ServerCancelled));
assert_eq!(message, "server cancelled the request");
assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
}
other => panic!("expected exhausted ServerCancelled error, got {other:?}"),
}
drop(guard);
let logs = captured.entries();
assert_eq!(
logs.iter()
.filter(|(level, _)| *level == tracing::Level::ERROR)
.count(),
1,
"exactly the final exhausted attempt must log at ERROR, got: {logs:?}"
);
assert!(
logs.iter()
.any(|(level, msg)| *level == tracing::Level::ERROR
&& msg.contains("LSP error response")
&& msg.contains("retries exhausted")),
"expected an ERROR log sharing the 'LSP error response' prefix and naming \
retry exhaustion, got: {logs:?}"
);
assert_eq!(
logs.iter()
.filter(
|(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
)
.count(),
usize::try_from(SERVER_CANCELLED_MAX_RETRIES).unwrap(),
"every attempt before the last must log a WARN 'will retry' line, got: {logs:?}"
);
}
#[tokio::test]
async fn test_retrigger_false_returns_immediately_without_retry() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ServerCancelled),
"server cancelled the request",
false,
)
.await;
let result = tokio::time::timeout(Duration::from_millis(200), request_task)
.await
.unwrap()
.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError { code, .. })) => {
assert_eq!(code, i32::from(LspErrorCodes::ServerCancelled));
}
other => panic!("expected immediate ServerCancelled error, got {other:?}"),
}
let second_request =
tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
.await;
assert!(
second_request.is_err(),
"no retry should have been sent after retriggerRequest: false"
);
}
#[tokio::test]
async fn test_retry_succeeds_after_one_server_cancelled_response() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
write_retryable_error_response(
&mut server.read_half_stdin,
&first["id"].clone(),
i32::from(LspErrorCodes::ServerCancelled),
"server cancelled the request",
true,
)
.await;
let second = read_framed_message(&mut reader).await;
assert_ne!(
first["id"], second["id"],
"retry must use a fresh request id"
);
let expected_result = serde_json::json!({ "contents": "resolved on retry" });
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
expected_result.clone(),
)
.await;
let result = request_task.await.unwrap();
assert_eq!(result.unwrap(), expected_result);
}
#[tokio::test]
async fn test_retry_exhaustion_returns_original_content_modified_error() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
for _ in 0..=SERVER_CANCELLED_MAX_RETRIES {
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
}
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError {
code,
message,
data,
})) => {
assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
assert_eq!(message, "content modified");
assert_eq!(data, Some(serde_json::json!({ "retriggerRequest": true })));
}
other => panic!("expected exhausted ContentModified error, got {other:?}"),
}
}
#[tokio::test]
async fn test_retrigger_false_returns_immediately_without_retry_for_content_modified() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
false,
)
.await;
let result = tokio::time::timeout(Duration::from_millis(200), request_task)
.await
.unwrap()
.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError { code, .. })) => {
assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
}
other => panic!("expected immediate ContentModified error, got {other:?}"),
}
let second_request =
tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
.await;
assert!(
second_request.is_err(),
"no retry should have been sent after retriggerRequest: false"
);
}
#[tokio::test]
async fn test_retry_succeeds_after_one_content_modified_response() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
write_retryable_error_response(
&mut server.read_half_stdin,
&first["id"].clone(),
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
let second = read_framed_message(&mut reader).await;
assert_ne!(
first["id"], second["id"],
"retry must use a fresh request id"
);
let expected_result = serde_json::json!({ "contents": "resolved on retry" });
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
expected_result.clone(),
)
.await;
let result = request_task.await.unwrap();
assert_eq!(result.unwrap(), expected_result);
}
#[tokio::test]
async fn test_retry_follows_the_error_of_a_frame_with_both_result_and_error() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
let both = serde_json::json!({
"jsonrpc": "2.0",
"id": first["id"],
"result": null,
"error": {
"code": i32::from(LspErrorCodes::ContentModified),
"message": "content modified",
"data": { "retriggerRequest": true },
},
})
.to_string();
let frame = format!("Content-Length: {}\r\n\r\n{both}", both.len());
server
.read_half_stdin
.write_all(frame.as_bytes())
.await
.unwrap();
server.read_half_stdin.flush().await.unwrap();
let second = read_framed_message(&mut reader).await;
assert_ne!(first["id"], second["id"]);
let expected = serde_json::json!({ "contents": "resolved on retry" });
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
expected.clone(),
)
.await;
assert_eq!(request_task.await.unwrap().unwrap(), expected);
}
#[tokio::test]
async fn test_content_modified_on_non_allowlisted_method_does_not_retry() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/rename",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
let result = tokio::time::timeout(Duration::from_millis(200), request_task)
.await
.unwrap()
.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError { code, .. })) => {
assert_eq!(code, i32::from(LspErrorCodes::ContentModified));
}
other => panic!("expected immediate ContentModified error, got {other:?}"),
}
let second_request =
tokio::time::timeout(Duration::from_millis(200), read_framed_message(&mut reader))
.await;
assert!(
second_request.is_err(),
"no retry should have been sent for a non-allowlisted method"
);
}
#[tokio::test]
async fn test_oversized_error_message_truncated_for_caller() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
let oversized_message = "x".repeat(MAX_ERROR_MESSAGE_CALLER_BYTES + 500);
write_error_response(&mut server.read_half_stdin, &id, -32603, &oversized_message)
.await;
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError { code, message, .. })) => {
assert_eq!(code, -32603);
assert!(
message.len() < oversized_message.len(),
"caller-facing message must be truncated, got {} bytes",
message.len()
);
assert!(message.ends_with("... (truncated)"));
}
other => panic!("expected truncated LspServerError, got {other:?}"),
}
}
#[tokio::test]
async fn test_error_message_between_log_and_caller_caps_reaches_caller_intact() {
let (client, mut server) = fake_lsp_client();
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
let message = "x".repeat(crate::util::MAX_LOG_STRING_BYTES + 50);
write_error_response(&mut server.read_half_stdin, &id, -32603, &message).await;
let result = request_task.await.unwrap();
match result {
Err(Error::LspServerError(JsonRpcError {
message: returned, ..
})) => {
assert_eq!(
returned, message,
"message under the caller cap must not be truncated"
);
}
other => panic!("expected untruncated LspServerError, got {other:?}"),
}
}
#[tokio::test]
async fn test_retried_error_that_recovers_does_not_log_error_level() {
use tracing_subscriber::layer::SubscriberExt as _;
let (client, mut server) = fake_lsp_client();
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let first = read_framed_message(&mut reader).await;
write_retryable_error_response(
&mut server.read_half_stdin,
&first["id"].clone(),
i32::from(LspErrorCodes::ServerCancelled),
"server cancelled the request",
true,
)
.await;
let second = read_framed_message(&mut reader).await;
write_success_response(
&mut server.read_half_stdin,
&second["id"].clone(),
serde_json::json!({ "contents": "resolved on retry" }),
)
.await;
let result = request_task.await.unwrap();
assert!(result.is_ok(), "expected retry to recover, got {result:?}");
drop(guard);
let logs = captured.entries();
assert!(
!logs
.iter()
.any(|(level, _)| *level == tracing::Level::ERROR),
"a retried-and-recovered error must not log at ERROR, got: {logs:?}"
);
assert!(
logs.iter().any(
|(level, msg)| *level == tracing::Level::WARN && msg.contains("will retry")
),
"expected a WARN 'will retry' log line, got: {logs:?}"
);
}
#[tokio::test]
async fn test_non_retryable_error_logs_error_level() {
use tracing_subscriber::layer::SubscriberExt as _;
let (client, mut server) = fake_lsp_client();
let captured = CapturedLogs::default();
let subscriber = tracing_subscriber::registry().with(captured.clone());
let guard = tracing::subscriber::set_default(subscriber);
let request_task = tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/rename",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let mut reader = BufReader::new(&mut server.write_stdout);
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_retryable_error_response(
&mut server.read_half_stdin,
&id,
i32::from(LspErrorCodes::ContentModified),
"content modified",
true,
)
.await;
let result = request_task.await.unwrap();
assert!(result.is_err(), "expected a non-retryable error");
drop(guard);
let logs = captured.entries();
assert!(
logs.iter()
.any(|(level, msg)| *level == tracing::Level::ERROR
&& msg.contains("LSP error response")
&& msg.contains("content modified")),
"a non-retryable error must still surface an ERROR log sharing the \
'LSP error response' prefix, got: {logs:?}"
);
}
}
mod reader_task_regression {
use tokio::io::BufReader;
use super::*;
use crate::test_lsp::{
fake_lsp_client, inert_transport, read_framed_message, write_response,
};
#[tokio::test]
async fn test_message_loop_inner_treats_reader_channel_close_as_server_terminated() {
let (mut transport, _reader) = inert_transport();
let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests = PendingRequests::default();
drop(msg_tx);
let result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
&Redactions::default(),
None,
)
.await;
assert_matches!(result, Err(Error::ServerTerminated), "got {result:?}");
}
#[tokio::test]
async fn test_message_loop_inner_reader_gone_then_drain_fails_pending_request() {
let (mut transport, _reader) = inert_transport();
let (_command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests = PendingRequests::default();
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests.register(RequestId::Number(1), response_tx);
drop(msg_tx);
let result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
&Redactions::default(),
None,
)
.await;
assert_matches!(result, Err(Error::ServerTerminated), "got {result:?}");
LspClient::drain_and_fail_pending(&pending_requests, &StdMutex::default());
let received = response_rx.await.unwrap();
assert_matches!(received, Err(Error::ServerTerminated), "got {received:?}");
assert!(pending_requests.no_waiting());
}
#[tokio::test]
async fn test_shutdown_drains_buffered_responses_instead_of_dropping_them() {
let (mut transport, _reader) = inert_transport();
let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests = PendingRequests::default();
let id = RequestId::Number(1);
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests.register(id.clone(), response_tx);
msg_tx
.send(Ok(InboundMessage::Response(JsonRpcResponse {
jsonrpc: JsonRpcVersion,
id: id.clone(),
outcome: JsonRpcOutcome::Result(serde_json::json!({ "ok": true })),
})))
.await
.unwrap();
command_tx.send(ClientCommand::Shutdown).await.unwrap();
drop(command_tx);
let result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
&Redactions::default(),
None,
)
.await;
assert!(result.is_ok(), "got {result:?}");
let received = response_rx.await.unwrap();
assert_eq!(received.unwrap(), serde_json::json!({ "ok": true }));
assert!(
pending_requests.no_waiting(),
"the drained response must resolve its pending request entry"
);
}
#[tokio::test]
async fn test_message_loop_fails_pending_requests_on_transport_error_exit() {
let (transport, reader) = inert_transport();
let (_command_tx, command_rx) = mpsc::channel::<ClientCommand>(1);
let pending_requests = PendingRequests::default();
let (response_tx, response_rx) = oneshot::channel::<Result<Value>>();
pending_requests.register(RequestId::Number(1), response_tx);
let result = LspClient::message_loop(
(transport, reader),
command_rx,
pending_requests.clone(),
Arc::default(),
None,
Arc::default(),
None,
)
.await;
assert!(result.is_err(), "got {result:?}");
let received = response_rx.await.unwrap();
assert_matches!(received, Err(Error::ServerTerminated), "got {received:?}");
assert!(pending_requests.no_waiting());
}
#[tokio::test]
async fn test_message_loop_shutdown_resolves_answered_and_fails_unanswered_pending() {
let (mut transport, _reader) = inert_transport();
let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(1);
let (msg_tx, mut msg_rx) = mpsc::channel::<Result<InboundMessage>>(1);
let pending_requests = PendingRequests::default();
let answered_id = RequestId::Number(1);
let (answered_tx, answered_rx) = oneshot::channel::<Result<Value>>();
let unanswered_id = RequestId::Number(2);
let (unanswered_tx, unanswered_rx) = oneshot::channel::<Result<Value>>();
pending_requests.register(answered_id.clone(), answered_tx);
pending_requests.register(unanswered_id.clone(), unanswered_tx);
msg_tx
.send(Ok(InboundMessage::Response(JsonRpcResponse {
jsonrpc: JsonRpcVersion,
id: answered_id,
outcome: JsonRpcOutcome::Result(serde_json::json!({ "ok": true })),
})))
.await
.unwrap();
command_tx.send(ClientCommand::Shutdown).await.unwrap();
drop(command_tx);
let inner_result = LspClient::message_loop_inner(
&mut transport,
&mut msg_rx,
&mut command_rx,
&pending_requests,
None,
&Redactions::default(),
None,
)
.await;
assert!(inner_result.is_ok(), "got {inner_result:?}");
LspClient::drain_and_fail_pending(&pending_requests, &StdMutex::default());
let answered = answered_rx.await.unwrap();
assert_eq!(answered.unwrap(), serde_json::json!({ "ok": true }));
let unanswered = unanswered_rx.await.unwrap();
assert_matches!(
unanswered,
Err(Error::ServerTerminated),
"got {unanswered:?}"
);
assert!(pending_requests.no_waiting());
}
#[tokio::test]
async fn test_message_loop_end_to_end_resolves_answered_then_fails_unanswered_on_shutdown()
{
let (client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let answered_client = client.clone();
let answered_task = tokio::spawn(async move {
answered_client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let request = read_framed_message(&mut reader).await;
let id = request["id"].clone();
write_response(
&mut server.read_half_stdin,
&id,
serde_json::json!({ "ok": true }),
)
.await;
let answered = answered_task.await.unwrap().unwrap();
assert_eq!(answered, serde_json::json!({ "ok": true }));
let unanswered_client = client.clone();
let unanswered_task = tokio::spawn(async move {
unanswered_client
.request::<_, Value>(
"textDocument/definition",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let _unanswered_request = read_framed_message(&mut reader).await;
client.shutdown().await.unwrap();
let unanswered = tokio::time::timeout(Duration::from_millis(200), unanswered_task)
.await
.unwrap_or_else(|_| {
panic!(
"unanswered request must fail immediately on shutdown, not hang until its own timeout"
)
})
.unwrap();
assert_matches!(
unanswered,
Err(Error::ServerTerminated),
"got {unanswered:?}"
);
}
#[tokio::test]
async fn test_dense_concurrent_requests_all_resolve_to_matching_responses() {
const REQUEST_COUNT: usize = 20;
let (client, mut server) = fake_lsp_client();
let mut request_tasks = Vec::with_capacity(REQUEST_COUNT);
for i in 0..REQUEST_COUNT {
let client = client.clone();
request_tasks.push(tokio::spawn(async move {
client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({ "n": i }),
Duration::from_secs(30),
)
.await
}));
}
let server_task = tokio::spawn(async move {
let mut reader = BufReader::new(&mut server.write_stdout);
let mut requests = Vec::with_capacity(REQUEST_COUNT);
for _ in 0..REQUEST_COUNT {
requests.push(read_framed_message(&mut reader).await);
}
for request in requests.into_iter().rev() {
let id = request["id"].clone();
let n = request["params"]["n"].clone();
write_response(
&mut server.read_half_stdin,
&id,
serde_json::json!({ "echo": n }),
)
.await;
}
});
server_task.await.unwrap();
for (i, task) in request_tasks.into_iter().enumerate() {
let value = task
.await
.unwrap()
.unwrap_or_else(|e| panic!("request {i} failed: {e:?}"));
assert_eq!(
value["echo"],
serde_json::json!(i),
"response for request {i} carried the wrong payload -- id/response mismatch"
);
}
}
#[tokio::test]
async fn test_shutdown_on_clone_stops_shared_loop_then_owner_shutdown_succeeds() {
let (client, _server) = fake_lsp_client();
client.clone().shutdown().await.unwrap();
let request = timeout(
Duration::from_secs(2),
client.request::<_, Value>("x/y", serde_json::json!({}), Duration::from_secs(30)),
)
.await
.unwrap_or_else(|_| panic!("request on a stopped loop must fail fast"));
assert_matches!(request, Err(Error::ServerTerminated), "got {request:?}");
client.shutdown().await.unwrap();
}
#[tokio::test]
async fn test_shutdown_until_expired_deadline_reports_timeout() {
let (mut client, _server) = fake_lsp_client();
let probe = client.clone();
let result = client
.shutdown_until(Instant::now() - Duration::from_millis(50))
.await;
assert_matches!(result, Err(Error::ShutdownTimeout), "got {result:?}");
assert_matches!(probe.state().await, crate::lsp::ServerState::Shutdown);
}
#[tokio::test]
async fn test_shutdown_until_aborts_wedged_loop_and_fails_pending() {
let (mut client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let in_flight_client = client.clone();
let in_flight = tokio::spawn(async move {
in_flight_client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let _ = read_framed_message(&mut reader).await;
client
.notify("x/wedge", serde_json::json!({ "p": "a".repeat(1 << 20) }))
.await
.unwrap();
let deadline = Instant::now() + Duration::from_millis(200);
let result = timeout(Duration::from_secs(5), client.shutdown_until(deadline))
.await
.unwrap_or_else(|_| panic!("shutdown_until must honor its deadline"));
assert_matches!(result, Err(Error::ShutdownTimeout), "got {result:?}");
let in_flight = timeout(Duration::from_secs(1), in_flight)
.await
.unwrap_or_else(|_| panic!("in-flight request must be failed by the drain"))
.unwrap();
assert_matches!(in_flight, Err(Error::ServerTerminated), "got {in_flight:?}");
}
#[tokio::test]
async fn test_marked_restarted_client_fails_pending_with_server_restarted() {
let (mut client, mut server) = fake_lsp_client();
let mut reader = BufReader::new(&mut server.write_stdout);
let in_flight_client = client.clone();
let in_flight = tokio::spawn(async move {
in_flight_client
.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
)
.await
});
let _ = read_framed_message(&mut reader).await;
client.mark_restarted(ServerId::from_static("rust"));
client
.shutdown_until(Instant::now() + Duration::from_millis(200))
.await
.ok();
let in_flight = timeout(Duration::from_secs(1), in_flight)
.await
.unwrap_or_else(|_| panic!("in-flight request must be failed by the drain"))
.unwrap();
assert!(
matches!(&in_flight, Err(Error::ServerRestarted { server_id }) if server_id.as_str() == "rust"),
"got {in_flight:?}"
);
}
#[tokio::test]
async fn test_request_after_shutdown_fails_fast_instead_of_hanging() {
let (client, _server) = fake_lsp_client();
let post_shutdown_client = client.clone();
client.shutdown().await.unwrap();
let result = tokio::time::timeout(
Duration::from_millis(200),
post_shutdown_client.request::<_, Value>(
"textDocument/hover",
serde_json::json!({}),
Duration::from_secs(30),
),
)
.await
.unwrap_or_else(|_| {
panic!(
"request after shutdown must fail immediately, not hang until its own timeout"
)
});
assert_matches!(result, Err(Error::ServerTerminated), "got {result:?}");
}
}
}