From 9a7d2c94c24792a448fab7c918e7af746c1cc899 Mon Sep 17 00:00:00 2001 From: imedfan <54060074+imedfan@users.noreply.github.com> Date: Tue, 7 Jul 2026 14:48:51 +0200 Subject: [PATCH] Handle JDTLS internal errors in proxy --- proxy/src/main.rs | 293 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 263 insertions(+), 30 deletions(-) diff --git a/proxy/src/main.rs b/proxy/src/main.rs index b07ce68..1960148 100644 --- a/proxy/src/main.rs +++ b/proxy/src/main.rs @@ -10,7 +10,7 @@ use decompile::{rewrite_jdt_in_strings, rewrite_jdt_locations}; use http::handle_http; use lsp::{parse_lsp_content, raw_has_id, write_raw, write_to_stdout, LspReader}; use platform::spawn_parent_monitor; -use serde_json::Value; +use serde_json::{json, Value}; use std::{ collections::HashMap, env, fs, @@ -26,9 +26,41 @@ use std::{ }; #[derive(Clone, Copy)] -enum TrackedKind { +enum RewriteKind { Definition, - Doc, + Documentation, +} + +#[derive(Clone)] +enum FallbackResult { + Null, + EmptyArray, + CompletionItem(Value), +} + +#[derive(Clone)] +struct TrackedRequest { + method: String, + rewrite: Option, + fallback: FallbackResult, +} + +impl TrackedRequest { + fn new(method: &str, rewrite: Option, fallback: FallbackResult) -> Self { + Self { + method: method.to_string(), + rewrite, + fallback, + } + } + + fn fallback_result(&self) -> Value { + match &self.fallback { + FallbackResult::Null => Value::Null, + FallbackResult::EmptyArray => json!([]), + FallbackResult::CompletionItem(item) => item.clone(), + } + } } fn main() { @@ -96,9 +128,10 @@ fn main() { let id_counter = Arc::new(AtomicU64::new(1)); - // Track definition/typeDefinition/implementation and documentation request IDs - // so their responses can be intercepted and rewritten. - let tracked_ids: Arc>> = Arc::new(Mutex::new(HashMap::new())); + // Track requests whose responses need rewriting or a soft fallback when + // JDTLS returns a JSON-RPC internal error. + let tracked_ids: Arc>> = + Arc::new(Mutex::new(HashMap::new())); // --- Thread 1: Zed stdin -> JDTLS stdin (track definition requests) --- let stdin_writer = Arc::clone(&child_stdin); @@ -115,23 +148,8 @@ fn main() { // textDocument/didChange. if raw_has_id(&raw) { if let Some(msg) = parse_lsp_content(&raw) { - if let Some(method) = msg.get("method").and_then(|m| m.as_str()) { - let kind = match method { - "textDocument/definition" - | "textDocument/typeDefinition" - | "textDocument/implementation" => { - Some(TrackedKind::Definition) - } - "textDocument/hover" - | "textDocument/signatureHelp" - | "completionItem/resolve" => Some(TrackedKind::Doc), - _ => None, - }; - if let Some(kind) = kind { - if let Some(id) = msg.get("id").cloned() { - tracked_in.lock().unwrap().insert(id, kind); - } - } + if let Some((id, request)) = tracked_request_for(&msg) { + tracked_in.lock().unwrap().insert(id, request); } } } @@ -179,11 +197,29 @@ fn main() { } } - // Rewrite jdt:// URIs in definition or documentation responses. - // Spawns a thread so this loop stays unblocked and can route - // the java/classFileContents response back via `pending`. + // Rewrite jdt:// URIs, or turn known JDTLS internal errors + // into harmless fallback results so one bad request doesn't + // break Java editing until the language server is restarted. if let Some(id) = msg.get("id").cloned() { - if let Some(kind) = tracked_out.lock().unwrap().remove(&id) { + if let Some(request) = tracked_out.lock().unwrap().remove(&id) { + if is_jdtls_internal_error(&msg) { + lsp_warn!( + "JDTLS internal error for {}; returning fallback result", + request.method + ); + write_to_stdout(&fallback_response(id, &request)); + continue; + } + + let Some(rewrite) = request.rewrite else { + write_raw(&mut io::stdout().lock(), &raw); + continue; + }; + let sanitize_signature_help = + request.method == "textDocument/signatureHelp"; + + // Spawns a thread so this loop stays unblocked and can route + // the java/classFileContents response back via `pending`. let writer = Arc::clone(&decompile_writer); let pending = Arc::clone(&decompile_pending); let pid = decompile_proxy_id.clone(); @@ -193,8 +229,8 @@ fn main() { let seq = counter.fetch_add(1, Ordering::Relaxed); Value::String(format!("{pid}-decompile-{seq}")) }; - match kind { - TrackedKind::Definition => { + match rewrite { + RewriteKind::Definition => { rewrite_jdt_locations( &mut msg, &writer, @@ -202,7 +238,7 @@ fn main() { &mut next_id, ); } - TrackedKind::Doc => { + RewriteKind::Documentation => { rewrite_jdt_in_strings( &mut msg, &writer, @@ -212,6 +248,9 @@ fn main() { sanitize_resolved_completion(&mut msg); } } + if sanitize_signature_help { + sanitize_signature_help_response(&mut msg); + } write_to_stdout(&msg); }); continue; @@ -272,3 +311,197 @@ fn main() { fn hex_encode(s: &str) -> String { s.as_bytes().iter().map(|b| format!("{b:02x}")).collect() } + +fn tracked_request_for(msg: &Value) -> Option<(Value, TrackedRequest)> { + let method = msg.get("method")?.as_str()?; + let id = msg.get("id")?.clone(); + let request = match method { + "textDocument/definition" + | "textDocument/typeDefinition" + | "textDocument/implementation" => { + TrackedRequest::new(method, Some(RewriteKind::Definition), FallbackResult::Null) + } + "textDocument/hover" | "textDocument/signatureHelp" => TrackedRequest::new( + method, + Some(RewriteKind::Documentation), + FallbackResult::Null, + ), + "completionItem/resolve" => TrackedRequest::new( + method, + Some(RewriteKind::Documentation), + FallbackResult::CompletionItem(msg.get("params").cloned().unwrap_or(Value::Null)), + ), + "textDocument/codeAction" | "textDocument/codeLens" | "textDocument/documentHighlight" => { + TrackedRequest::new(method, None, FallbackResult::EmptyArray) + } + _ => return None, + }; + + Some((id, request)) +} + +fn is_jdtls_internal_error(msg: &Value) -> bool { + let Some(error) = msg.get("error") else { + return false; + }; + + let has_internal_error_code = error + .get("code") + .and_then(|code| code.as_i64()) + .is_some_and(|code| code == -32603); + let has_internal_error_message = error + .get("message") + .and_then(|message| message.as_str()) + .is_some_and(|message| message.to_ascii_lowercase().contains("internal error")); + + has_internal_error_code || has_internal_error_message +} + +fn fallback_response(id: Value, request: &TrackedRequest) -> Value { + json!({ + "jsonrpc": "2.0", + "id": id, + "result": request.fallback_result(), + }) +} + +fn sanitize_signature_help_response(msg: &mut Value) { + let Some(result) = msg + .get_mut("result") + .and_then(|result| result.as_object_mut()) + else { + return; + }; + + let has_negative_active_parameter = result + .get("activeParameter") + .and_then(|active_parameter| active_parameter.as_i64()) + .is_some_and(|active_parameter| active_parameter < 0); + + if has_negative_active_parameter { + result.remove("activeParameter"); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_jdtls_internal_error_by_code() { + let msg = json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32603, + "message": "Request failed" + } + }); + + assert!(is_jdtls_internal_error(&msg)); + } + + #[test] + fn detects_jdtls_internal_error_by_message() { + let msg = json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": 0, + "message": "Internal error." + } + }); + + assert!(is_jdtls_internal_error(&msg)); + } + + #[test] + fn ignores_non_internal_errors() { + let msg = json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32800, + "message": "Request cancelled" + } + }); + + assert!(!is_jdtls_internal_error(&msg)); + } + + #[test] + fn builds_empty_array_fallback_response() { + let request = + TrackedRequest::new("textDocument/codeAction", None, FallbackResult::EmptyArray); + + assert_eq!( + fallback_response(json!(7), &request), + json!({ + "jsonrpc": "2.0", + "id": 7, + "result": [] + }) + ); + } + + #[test] + fn completion_resolve_fallback_returns_original_item() { + let request = json!({ + "jsonrpc": "2.0", + "id": 2, + "method": "completionItem/resolve", + "params": { + "label": "String", + "kind": 7 + } + }); + + let (id, tracked) = tracked_request_for(&request).unwrap(); + + assert_eq!( + fallback_response(id, &tracked), + json!({ + "jsonrpc": "2.0", + "id": 2, + "result": { + "label": "String", + "kind": 7 + } + }) + ); + } + + #[test] + fn removes_negative_signature_help_active_parameter() { + let mut msg = json!({ + "jsonrpc": "2.0", + "id": 3, + "result": { + "signatures": [], + "activeSignature": 0, + "activeParameter": -1 + } + }); + + sanitize_signature_help_response(&mut msg); + + assert!(msg["result"].get("activeParameter").is_none()); + } + + #[test] + fn preserves_valid_signature_help_active_parameter() { + let mut msg = json!({ + "jsonrpc": "2.0", + "id": 3, + "result": { + "signatures": [], + "activeSignature": 0, + "activeParameter": 1 + } + }); + + sanitize_signature_help_response(&mut msg); + + assert_eq!(msg["result"]["activeParameter"], json!(1)); + } +}