Skip to content

Commit 9a7d2c9

Browse files
committed
Handle JDTLS internal errors in proxy
1 parent 55db303 commit 9a7d2c9

1 file changed

Lines changed: 263 additions & 30 deletions

File tree

proxy/src/main.rs

Lines changed: 263 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use decompile::{rewrite_jdt_in_strings, rewrite_jdt_locations};
1010
use http::handle_http;
1111
use lsp::{parse_lsp_content, raw_has_id, write_raw, write_to_stdout, LspReader};
1212
use platform::spawn_parent_monitor;
13-
use serde_json::Value;
13+
use serde_json::{json, Value};
1414
use std::{
1515
collections::HashMap,
1616
env, fs,
@@ -26,9 +26,41 @@ use std::{
2626
};
2727

2828
#[derive(Clone, Copy)]
29-
enum TrackedKind {
29+
enum RewriteKind {
3030
Definition,
31-
Doc,
31+
Documentation,
32+
}
33+
34+
#[derive(Clone)]
35+
enum FallbackResult {
36+
Null,
37+
EmptyArray,
38+
CompletionItem(Value),
39+
}
40+
41+
#[derive(Clone)]
42+
struct TrackedRequest {
43+
method: String,
44+
rewrite: Option<RewriteKind>,
45+
fallback: FallbackResult,
46+
}
47+
48+
impl TrackedRequest {
49+
fn new(method: &str, rewrite: Option<RewriteKind>, fallback: FallbackResult) -> Self {
50+
Self {
51+
method: method.to_string(),
52+
rewrite,
53+
fallback,
54+
}
55+
}
56+
57+
fn fallback_result(&self) -> Value {
58+
match &self.fallback {
59+
FallbackResult::Null => Value::Null,
60+
FallbackResult::EmptyArray => json!([]),
61+
FallbackResult::CompletionItem(item) => item.clone(),
62+
}
63+
}
3264
}
3365

3466
fn main() {
@@ -96,9 +128,10 @@ fn main() {
96128

97129
let id_counter = Arc::new(AtomicU64::new(1));
98130

99-
// Track definition/typeDefinition/implementation and documentation request IDs
100-
// so their responses can be intercepted and rewritten.
101-
let tracked_ids: Arc<Mutex<HashMap<Value, TrackedKind>>> = Arc::new(Mutex::new(HashMap::new()));
131+
// Track requests whose responses need rewriting or a soft fallback when
132+
// JDTLS returns a JSON-RPC internal error.
133+
let tracked_ids: Arc<Mutex<HashMap<Value, TrackedRequest>>> =
134+
Arc::new(Mutex::new(HashMap::new()));
102135

103136
// --- Thread 1: Zed stdin -> JDTLS stdin (track definition requests) ---
104137
let stdin_writer = Arc::clone(&child_stdin);
@@ -115,23 +148,8 @@ fn main() {
115148
// textDocument/didChange.
116149
if raw_has_id(&raw) {
117150
if let Some(msg) = parse_lsp_content(&raw) {
118-
if let Some(method) = msg.get("method").and_then(|m| m.as_str()) {
119-
let kind = match method {
120-
"textDocument/definition"
121-
| "textDocument/typeDefinition"
122-
| "textDocument/implementation" => {
123-
Some(TrackedKind::Definition)
124-
}
125-
"textDocument/hover"
126-
| "textDocument/signatureHelp"
127-
| "completionItem/resolve" => Some(TrackedKind::Doc),
128-
_ => None,
129-
};
130-
if let Some(kind) = kind {
131-
if let Some(id) = msg.get("id").cloned() {
132-
tracked_in.lock().unwrap().insert(id, kind);
133-
}
134-
}
151+
if let Some((id, request)) = tracked_request_for(&msg) {
152+
tracked_in.lock().unwrap().insert(id, request);
135153
}
136154
}
137155
}
@@ -179,11 +197,29 @@ fn main() {
179197
}
180198
}
181199

182-
// Rewrite jdt:// URIs in definition or documentation responses.
183-
// Spawns a thread so this loop stays unblocked and can route
184-
// the java/classFileContents response back via `pending`.
200+
// Rewrite jdt:// URIs, or turn known JDTLS internal errors
201+
// into harmless fallback results so one bad request doesn't
202+
// break Java editing until the language server is restarted.
185203
if let Some(id) = msg.get("id").cloned() {
186-
if let Some(kind) = tracked_out.lock().unwrap().remove(&id) {
204+
if let Some(request) = tracked_out.lock().unwrap().remove(&id) {
205+
if is_jdtls_internal_error(&msg) {
206+
lsp_warn!(
207+
"JDTLS internal error for {}; returning fallback result",
208+
request.method
209+
);
210+
write_to_stdout(&fallback_response(id, &request));
211+
continue;
212+
}
213+
214+
let Some(rewrite) = request.rewrite else {
215+
write_raw(&mut io::stdout().lock(), &raw);
216+
continue;
217+
};
218+
let sanitize_signature_help =
219+
request.method == "textDocument/signatureHelp";
220+
221+
// Spawns a thread so this loop stays unblocked and can route
222+
// the java/classFileContents response back via `pending`.
187223
let writer = Arc::clone(&decompile_writer);
188224
let pending = Arc::clone(&decompile_pending);
189225
let pid = decompile_proxy_id.clone();
@@ -193,16 +229,16 @@ fn main() {
193229
let seq = counter.fetch_add(1, Ordering::Relaxed);
194230
Value::String(format!("{pid}-decompile-{seq}"))
195231
};
196-
match kind {
197-
TrackedKind::Definition => {
232+
match rewrite {
233+
RewriteKind::Definition => {
198234
rewrite_jdt_locations(
199235
&mut msg,
200236
&writer,
201237
&pending,
202238
&mut next_id,
203239
);
204240
}
205-
TrackedKind::Doc => {
241+
RewriteKind::Documentation => {
206242
rewrite_jdt_in_strings(
207243
&mut msg,
208244
&writer,
@@ -212,6 +248,9 @@ fn main() {
212248
sanitize_resolved_completion(&mut msg);
213249
}
214250
}
251+
if sanitize_signature_help {
252+
sanitize_signature_help_response(&mut msg);
253+
}
215254
write_to_stdout(&msg);
216255
});
217256
continue;
@@ -272,3 +311,197 @@ fn main() {
272311
fn hex_encode(s: &str) -> String {
273312
s.as_bytes().iter().map(|b| format!("{b:02x}")).collect()
274313
}
314+
315+
fn tracked_request_for(msg: &Value) -> Option<(Value, TrackedRequest)> {
316+
let method = msg.get("method")?.as_str()?;
317+
let id = msg.get("id")?.clone();
318+
let request = match method {
319+
"textDocument/definition"
320+
| "textDocument/typeDefinition"
321+
| "textDocument/implementation" => {
322+
TrackedRequest::new(method, Some(RewriteKind::Definition), FallbackResult::Null)
323+
}
324+
"textDocument/hover" | "textDocument/signatureHelp" => TrackedRequest::new(
325+
method,
326+
Some(RewriteKind::Documentation),
327+
FallbackResult::Null,
328+
),
329+
"completionItem/resolve" => TrackedRequest::new(
330+
method,
331+
Some(RewriteKind::Documentation),
332+
FallbackResult::CompletionItem(msg.get("params").cloned().unwrap_or(Value::Null)),
333+
),
334+
"textDocument/codeAction" | "textDocument/codeLens" | "textDocument/documentHighlight" => {
335+
TrackedRequest::new(method, None, FallbackResult::EmptyArray)
336+
}
337+
_ => return None,
338+
};
339+
340+
Some((id, request))
341+
}
342+
343+
fn is_jdtls_internal_error(msg: &Value) -> bool {
344+
let Some(error) = msg.get("error") else {
345+
return false;
346+
};
347+
348+
let has_internal_error_code = error
349+
.get("code")
350+
.and_then(|code| code.as_i64())
351+
.is_some_and(|code| code == -32603);
352+
let has_internal_error_message = error
353+
.get("message")
354+
.and_then(|message| message.as_str())
355+
.is_some_and(|message| message.to_ascii_lowercase().contains("internal error"));
356+
357+
has_internal_error_code || has_internal_error_message
358+
}
359+
360+
fn fallback_response(id: Value, request: &TrackedRequest) -> Value {
361+
json!({
362+
"jsonrpc": "2.0",
363+
"id": id,
364+
"result": request.fallback_result(),
365+
})
366+
}
367+
368+
fn sanitize_signature_help_response(msg: &mut Value) {
369+
let Some(result) = msg
370+
.get_mut("result")
371+
.and_then(|result| result.as_object_mut())
372+
else {
373+
return;
374+
};
375+
376+
let has_negative_active_parameter = result
377+
.get("activeParameter")
378+
.and_then(|active_parameter| active_parameter.as_i64())
379+
.is_some_and(|active_parameter| active_parameter < 0);
380+
381+
if has_negative_active_parameter {
382+
result.remove("activeParameter");
383+
}
384+
}
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::*;
389+
390+
#[test]
391+
fn detects_jdtls_internal_error_by_code() {
392+
let msg = json!({
393+
"jsonrpc": "2.0",
394+
"id": 1,
395+
"error": {
396+
"code": -32603,
397+
"message": "Request failed"
398+
}
399+
});
400+
401+
assert!(is_jdtls_internal_error(&msg));
402+
}
403+
404+
#[test]
405+
fn detects_jdtls_internal_error_by_message() {
406+
let msg = json!({
407+
"jsonrpc": "2.0",
408+
"id": 1,
409+
"error": {
410+
"code": 0,
411+
"message": "Internal error."
412+
}
413+
});
414+
415+
assert!(is_jdtls_internal_error(&msg));
416+
}
417+
418+
#[test]
419+
fn ignores_non_internal_errors() {
420+
let msg = json!({
421+
"jsonrpc": "2.0",
422+
"id": 1,
423+
"error": {
424+
"code": -32800,
425+
"message": "Request cancelled"
426+
}
427+
});
428+
429+
assert!(!is_jdtls_internal_error(&msg));
430+
}
431+
432+
#[test]
433+
fn builds_empty_array_fallback_response() {
434+
let request =
435+
TrackedRequest::new("textDocument/codeAction", None, FallbackResult::EmptyArray);
436+
437+
assert_eq!(
438+
fallback_response(json!(7), &request),
439+
json!({
440+
"jsonrpc": "2.0",
441+
"id": 7,
442+
"result": []
443+
})
444+
);
445+
}
446+
447+
#[test]
448+
fn completion_resolve_fallback_returns_original_item() {
449+
let request = json!({
450+
"jsonrpc": "2.0",
451+
"id": 2,
452+
"method": "completionItem/resolve",
453+
"params": {
454+
"label": "String",
455+
"kind": 7
456+
}
457+
});
458+
459+
let (id, tracked) = tracked_request_for(&request).unwrap();
460+
461+
assert_eq!(
462+
fallback_response(id, &tracked),
463+
json!({
464+
"jsonrpc": "2.0",
465+
"id": 2,
466+
"result": {
467+
"label": "String",
468+
"kind": 7
469+
}
470+
})
471+
);
472+
}
473+
474+
#[test]
475+
fn removes_negative_signature_help_active_parameter() {
476+
let mut msg = json!({
477+
"jsonrpc": "2.0",
478+
"id": 3,
479+
"result": {
480+
"signatures": [],
481+
"activeSignature": 0,
482+
"activeParameter": -1
483+
}
484+
});
485+
486+
sanitize_signature_help_response(&mut msg);
487+
488+
assert!(msg["result"].get("activeParameter").is_none());
489+
}
490+
491+
#[test]
492+
fn preserves_valid_signature_help_active_parameter() {
493+
let mut msg = json!({
494+
"jsonrpc": "2.0",
495+
"id": 3,
496+
"result": {
497+
"signatures": [],
498+
"activeSignature": 0,
499+
"activeParameter": 1
500+
}
501+
});
502+
503+
sanitize_signature_help_response(&mut msg);
504+
505+
assert_eq!(msg["result"]["activeParameter"], json!(1));
506+
}
507+
}

0 commit comments

Comments
 (0)