eclipse-lsp4j / lsp4j

A Java implementation of the language server protocol intended to be consumed by tools and language servers implemented in Java.
https://eclipse.org/lsp4j
Other
613 stars 145 forks source link

JsonIOException CompletableFuture#result in MessageTypeAdapter #680

Closed N1k145 closed 1 year ago

N1k145 commented 1 year ago

Hey, I'm getting a similar issue to #674 using 0.15.0 running on Java 17 in the current eclipse release (2022-09).

Nov 03, 2022 2:03:58 PM org.eclipse.lsp4j.jsonrpc.RemoteEndpoint fallbackResponseError
SEVERE: Internal error: com.google.gson.JsonIOException: Failed making field 'java.util.concurrent.CompletableFuture#result' accessible; either change its visibility or write a custom TypeAdapter for its declaring type
java.util.concurrent.CompletionException: com.google.gson.JsonIOException: Failed making field 'java.util.concurrent.CompletableFuture#result' accessible; either change its visibility or write a custom TypeAdapter for its declaring type
    at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
    at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
    at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:722)
    at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
    at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147)
    at org.eclipse.xtext.ide.server.concurrent.AbstractRequest.complete(AbstractRequest.java:65)
    at org.eclipse.xtext.ide.server.concurrent.ReadRequest.lambda$doRun$0(ReadRequest.java:67)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: com.google.gson.JsonIOException: Failed making field 'java.util.concurrent.CompletableFuture#result' accessible; either change its visibility or write a custom TypeAdapter for its declaring type
    at com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:23)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:203)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:112)
    at com.google.gson.Gson.getAdapter(Gson.java:531)
    at com.google.gson.Gson.toJson(Gson.java:778)
    at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.write(MessageTypeAdapter.java:423)
    at org.eclipse.lsp4j.jsonrpc.json.adapters.MessageTypeAdapter.write(MessageTypeAdapter.java:55)
    at com.google.gson.Gson.toJson(Gson.java:786)
    at com.google.gson.Gson.toJson(Gson.java:756)
    at org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler.serialize(MessageJsonHandler.java:145)
    at org.eclipse.lsp4j.jsonrpc.json.MessageJsonHandler.serialize(MessageJsonHandler.java:140)
    at org.eclipse.lsp4j.jsonrpc.json.StreamMessageConsumer.consume(StreamMessageConsumer.java:59)
    at org.eclipse.lsp4j.jsonrpc.RemoteEndpoint.lambda$handleRequest$1(RemoteEndpoint.java:281)
    at java.base/java.util.concurrent.CompletableFuture$UniAccept.tryFire(CompletableFuture.java:718)
    ... 9 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field volatile java.lang.Object java.util.concurrent.CompletableFuture.result accessible: module java.base does not "opens java.util.concurrent" to unnamed module @6dd70c49
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
    at com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:20)
    ... 22 more

Adding --add-opens java.base/java.util.concurrent=ALL-UNNAMED fixes this for now, but that's of course not a long time solution.

jonahgraham commented 1 year ago

Do you have a log or know what the message was that caused the exception?

jonahgraham commented 1 year ago

I tried running the testsuite with Java 17 and nothing failed.

Without more information I can't quite tell, but it looks to me like the Java code is trying to send a result (ResponseMessage) back to the other end and that message's result is of type CompletableFuture, which isn't really a valid type to serialize. It may work with the add-opens because you end up with the CompletableFuture's result field in the output.

That said, I could actually reproduce this, but not with any standard LSP, but with some custom server code (by adapting IntegrationTest).

    public static interface MyServerIssue680 {
        @JsonRequest
        CompletableFuture<Object>  askServer(MyParam param);
    }

    public static class MyServerIssue680Impl implements MyServerIssue680 {
        private Object innerMethodDoingTheWork(MyParam param) {
            return CompletableFuture.completedFuture(param);
        }

        @Override
        public CompletableFuture<Object> askServer(MyParam param) {
            return CompletableFuture.completedFuture(innerMethodDoingTheWork(param));
        }
    };

    @Test
    public void testResponseIssue680() throws Exception {
        // create client message
        String requestMessage = "{\"jsonrpc\": \"2.0\",\n"
                + "\"id\": \"42\",\n" 
                + "\"method\": \"askServer\",\n" 
                + "\"params\": { \"value\": \"bar\" }\n"
                + "}";
        String clientMessage = getHeader(requestMessage.getBytes().length) + requestMessage;

        // create server side
        ByteArrayInputStream in = new ByteArrayInputStream(clientMessage.getBytes());
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        MyServerIssue680 server = new MyServerIssue680Impl();
        Launcher<MyClient> serverSideLauncher = Launcher.createLauncher(server, MyClient.class, in, out);
        serverSideLauncher.startListening().get(TIMEOUT, TimeUnit.MILLISECONDS);

        Assert.assertEquals("Content-Length: 63" + CRLF + CRLF
                + "{\"jsonrpc\":\"2.0\",\"id\":\"42\",\"result\":{\"result\":{\"value\":\"bar\"}}}",
                out.toString());
    }

    protected String getHeader(int contentLength) {
        StringBuilder headerBuilder = new StringBuilder();
        headerBuilder.append(CONTENT_LENGTH_HEADER).append(": ").append(contentLength).append(CRLF);
        headerBuilder.append(CRLF);
        return headerBuilder.toString();
    }

On Java 11 that passes, on Java 17 (without the --add-opens) it fails with a nearly identical stack trace.

To me that make sense as a CompletableFuture cannot be returned over the JSON-RPC link. You can see there is an issue in the JSON itself as it has a duplicated/nested result field:

{
    "jsonrpc": "2.0", 
    "id": "42", 
    "result": { 
        "result": { 
            "value": "bar" 
        } 
    } 
}

Does the above describe your scenario? If so, it sounds like there is a bug report that should be filed with the project that has LSP4J integrated into it.

N1k145 commented 1 year ago

@jonahgraham thank for looking into it, and sorry for wasting your time. We found the issue in our integration. We were returning the Completable Future as a result of a command execution, which was actually executing the command and not the result of the Future.