You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@thrift.apache.org by je...@apache.org on 2022/03/02 21:15:04 UTC

[thrift] branch master updated: THRIFT-5519 Java async client loses exceptions in void methods Client: java

This is an automated email from the ASF dual-hosted git repository.

jensg pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/thrift.git


The following commit(s) were added to refs/heads/master by this push:
     new c9b7bd7  THRIFT-5519 Java async client loses exceptions in void methods Client: java
c9b7bd7 is described below

commit c9b7bd7b1285e8feb902daa208eacd7655a380ee
Author: Alex Kormukhin <a....@yandex.ru>
AuthorDate: Fri Feb 18 21:04:14 2022 +0300

    THRIFT-5519 Java async client loses exceptions in void methods
    Client: java
    
    (cherry picked from commit c4d3e36ed4db97fb6213cc13a4e611a4e658b4b7)
---
 .../cpp/src/thrift/generate/t_java_generator.cc    |   4 +
 lib/java/gradle/generateTestThrift.gradle          |   1 +
 .../test/voidmethexceptions/ServiceAsyncImp.java   |  85 ++++
 .../test/voidmethexceptions/ServiceBase.java       |  44 ++
 .../test/voidmethexceptions/ServiceSyncImp.java    |  72 +++
 .../voidmethexceptions/TestVoidMethExceptions.java | 549 +++++++++++++++++++++
 test/Makefile.am                                   |   1 +
 test/VoidMethExceptionsTest.thrift                 |  13 +
 8 files changed, 769 insertions(+)

diff --git a/compiler/cpp/src/thrift/generate/t_java_generator.cc b/compiler/cpp/src/thrift/generate/t_java_generator.cc
index ceabe66..3fa5f57 100644
--- a/compiler/cpp/src/thrift/generate/t_java_generator.cc
+++ b/compiler/cpp/src/thrift/generate/t_java_generator.cc
@@ -3250,6 +3250,10 @@ void t_java_generator::generate_service_async_client(t_service* tservice) {
                        "client.getProtocolFactory().getProtocol(memoryTransport);" << endl;
     indent(f_service_);
     if (ret_type->is_void()) { // NB: Includes oneways which always return void.
+      if (!(*f_iter)->is_oneway()) {
+        f_service_ << "(new Client(prot)).recv" + sep + javaname + "();" << endl;
+        indent(f_service_);
+      }
       f_service_ << "return null;" << endl;
     } else {
       f_service_ << "return (new Client(prot)).recv" + sep + javaname + "();" << endl;
diff --git a/lib/java/gradle/generateTestThrift.gradle b/lib/java/gradle/generateTestThrift.gradle
index 4b712ca..996c5fa 100644
--- a/lib/java/gradle/generateTestThrift.gradle
+++ b/lib/java/gradle/generateTestThrift.gradle
@@ -81,6 +81,7 @@ task generateJava(group: 'Build') {
     thriftCompile(it, 'JavaDeepCopyTest.thrift')
     thriftCompile(it, 'EnumContainersTest.thrift')
     thriftCompile(it, 'JavaBinaryDefault.thrift')
+    thriftCompile(it, 'VoidMethExceptionsTest.thrift')
     thriftCompile(it, 'partial/thrift_test_schema.thrift')
 }
 
diff --git a/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceAsyncImp.java b/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceAsyncImp.java
new file mode 100644
index 0000000..419e327
--- /dev/null
+++ b/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceAsyncImp.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.thrift.test.voidmethexceptions;
+
+import org.apache.thrift.TApplicationException;
+import org.apache.thrift.TException;
+import org.apache.thrift.async.AsyncMethodCallback;
+import thrift.test.voidmethexceptions.TAppService01;
+import thrift.test.voidmethexceptions.TExampleException;
+
+public class ServiceAsyncImp extends ServiceBase implements TAppService01.AsyncIface {
+
+  @Override
+  public void returnString(String msg,
+      boolean throwException,
+      AsyncMethodCallback<String> resultHandler) throws TException {
+    if (throwException) {
+      resultHandler.onError(new TExampleException(msg));
+    } else {
+      resultHandler.onComplete(msg);
+    }
+  }
+
+  @Override
+  public void returnVoidThrows(String msg,
+      boolean throwException,
+      AsyncMethodCallback<Void> resultHandler) throws TException {
+    if (throwException) {
+      resultHandler.onError(new TExampleException(msg));
+    } else {
+      resultHandler.onComplete(null);
+    }
+  }
+
+  @Override
+  public void returnVoidNoThrowsRuntimeException(String msg,
+      boolean throwException,
+      AsyncMethodCallback<Void> resultHandler) throws TException {
+    if (throwException) {
+      resultHandler.onError(new RuntimeException(msg));
+    } else {
+      resultHandler.onComplete(null);
+    }
+  }
+
+  @Override
+  public void returnVoidNoThrowsTApplicationException(String msg,
+      boolean throwException,
+      AsyncMethodCallback<Void> resultHandler) throws TException {
+    if (throwException) {
+      resultHandler.onError(new TApplicationException(TApplicationException.INTERNAL_ERROR, msg));
+    } else {
+      resultHandler.onComplete(null);
+    }
+  }
+
+  @Override
+  public void onewayVoidNoThrows(String msg,
+      boolean throwException,
+      AsyncMethodCallback<Void> resultHandler) throws TException {
+    if (throwException) {
+      resultHandler.onError(new TApplicationException(TApplicationException.INTERNAL_ERROR, msg));
+    } else {
+      // simulate hang up
+    }
+  }
+
+}
diff --git a/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceBase.java b/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceBase.java
new file mode 100644
index 0000000..5502b09
--- /dev/null
+++ b/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceBase.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.thrift.test.voidmethexceptions;
+
+public class ServiceBase {
+
+  private volatile boolean cancelled = false;
+
+  public boolean isCancelled() {
+    return cancelled;
+  }
+
+  public void setCancelled(boolean cancelled) {
+    this.cancelled = cancelled;
+  }
+
+  protected void waitForCancel() {
+    while (!isCancelled()) {
+      try {
+        Thread.sleep(10L);
+      } catch (InterruptedException x) {
+        break;
+      }
+    }
+  }
+
+}
diff --git a/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceSyncImp.java b/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceSyncImp.java
new file mode 100644
index 0000000..a8a08b5
--- /dev/null
+++ b/lib/java/test/org/apache/thrift/test/voidmethexceptions/ServiceSyncImp.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.thrift.test.voidmethexceptions;
+
+import org.apache.thrift.TApplicationException;
+import org.apache.thrift.TException;
+import thrift.test.voidmethexceptions.TAppService01;
+import thrift.test.voidmethexceptions.TExampleException;
+
+public class ServiceSyncImp extends ServiceBase implements TAppService01.Iface {
+
+  @Override
+  public String returnString(String msg,
+      boolean throwException) throws TExampleException, TException {
+    if (throwException) {
+      throw new TExampleException(msg);
+    }
+    return msg;
+  }
+
+  @Override
+  public void returnVoidThrows(String msg,
+      boolean throwException) throws TExampleException, TException {
+    if (throwException) {
+      throw new TExampleException(msg);
+    }
+  }
+
+  @Override
+  public void returnVoidNoThrowsRuntimeException(String msg,
+      boolean throwException) throws TException {
+    if (throwException) {
+      throw new RuntimeException(msg);
+    }
+  }
+
+  @Override
+  public void returnVoidNoThrowsTApplicationException(String msg,
+      boolean throwException) throws TException {
+    if (throwException) {
+      throw new TApplicationException(TApplicationException.INTERNAL_ERROR, msg);
+    }
+  }
+
+  @Override
+  public void onewayVoidNoThrows(String msg, boolean throwException) throws TException {
+    if (throwException) {
+      throw new TApplicationException(TApplicationException.INTERNAL_ERROR, msg);
+    } else {
+      // simulate hang up
+      waitForCancel();
+    }
+  }
+
+}
diff --git a/lib/java/test/org/apache/thrift/test/voidmethexceptions/TestVoidMethExceptions.java b/lib/java/test/org/apache/thrift/test/voidmethexceptions/TestVoidMethExceptions.java
new file mode 100644
index 0000000..af39262
--- /dev/null
+++ b/lib/java/test/org/apache/thrift/test/voidmethexceptions/TestVoidMethExceptions.java
@@ -0,0 +1,549 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.thrift.test.voidmethexceptions;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.thrift.TApplicationException;
+import org.apache.thrift.TConfiguration;
+import org.apache.thrift.TProcessor;
+import org.apache.thrift.async.AsyncMethodCallback;
+import org.apache.thrift.async.TAsyncClientManager;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.apache.thrift.server.TNonblockingServer;
+import org.apache.thrift.server.TServer;
+import org.apache.thrift.transport.TNonblockingServerSocket;
+import org.apache.thrift.transport.TNonblockingSocket;
+import org.apache.thrift.transport.TSocket;
+import org.apache.thrift.transport.TTransport;
+import org.apache.thrift.transport.layered.TFramedTransport;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import thrift.test.voidmethexceptions.TAppService01;
+import thrift.test.voidmethexceptions.TExampleException;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+@RunWith(Parameterized.class)
+public class TestVoidMethExceptions {
+
+  private static final Logger log = LoggerFactory.getLogger(TestVoidMethExceptions.class);
+
+  private static final int TIMEOUT_MILLIS = 5_000;
+
+  private final ServerImplementationType serverImplementationType;
+
+  private TServer server;
+  private Thread serverThread;
+  private int serverPort;
+
+
+  public TestVoidMethExceptions(ServerImplementationType serverImplementationType) {
+    Assert.assertNotNull(serverImplementationType);
+    this.serverImplementationType = serverImplementationType;
+  }
+
+
+  @Parameters(name = "serverImplementationType = {0}")
+  public static Object[][] parameters() {
+    return new Object[][]{{ServerImplementationType.SYNC_SERVER},
+        {ServerImplementationType.ASYNC_SERVER}};
+  }
+
+
+  @Before
+  public void setUp() throws Exception {
+    serverPort = -1;
+    serverImplementationType.service.setCancelled(false);
+    CompletableFuture<Void> futureServerStarted = new CompletableFuture<>();
+    TNonblockingServerSocket serverTransport = new TNonblockingServerSocket(0);
+    TNonblockingServer.Args args = new TNonblockingServer.Args(serverTransport);
+    args.processor(serverImplementationType.processor);
+    server = new TNonblockingServer(args) {
+
+      @Override
+      protected void setServing(boolean serving) {
+        super.setServing(serving);
+
+        if (serving) {
+          serverPort = serverTransport.getPort();
+          futureServerStarted.complete(null);
+        }
+      }
+
+    };
+
+    serverThread = new Thread(() -> {
+      server.serve();
+    }, "thrift-server");
+    serverThread.setDaemon(true);
+    serverThread.start();
+    futureServerStarted.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    serverImplementationType.service.setCancelled(true);
+    server.stop();
+    serverThread.join(TIMEOUT_MILLIS);
+  }
+
+
+  @Test
+  public void testSyncClientMustReturnResultReturnString() throws Exception {
+    checkSyncClient("returnString",
+        "sent msg",
+        false,
+        "sent msg",
+        null,
+        null,
+        (client, msg, throwException) -> {
+          return client.returnString(msg, throwException);
+        });
+  }
+
+  @Test
+  public void testSyncClientMustReturnResultReturnVoidThrows() throws Exception {
+    checkSyncClient("returnVoidThrows",
+        "sent msg",
+        false,
+        null,
+        null,
+        null,
+        (client, msg, throwException) -> {
+          client.returnVoidThrows(msg, throwException);
+          return null;
+        });
+  }
+
+  @Test
+  public void testSyncClientMustReturnResultReturnVoidNoThrowsRuntimeException() throws Exception {
+    checkSyncClient("returnVoidNoThrowsRuntimeException",
+        "sent msg",
+        false,
+        null,
+        null,
+        null,
+        (client, msg, throwException) -> {
+          client.returnVoidNoThrowsRuntimeException(msg, throwException);
+          return null;
+        });
+  }
+
+  @Test
+  public void testSyncClientMustReturnResultReturnVoidNoThrowsTApplicationException() throws Exception {
+    checkSyncClient("returnVoidNoThrowsTApplicationException",
+        "sent msg",
+        false,
+        null,
+        null,
+        null,
+        (client, msg, throwException) -> {
+          client.returnVoidNoThrowsTApplicationException(msg, throwException);
+          return null;
+        });
+  }
+
+
+  @Test
+  public void testSyncClientMustThrowExceptionReturnString() throws Exception {
+    checkSyncClient("returnString",
+        "sent msg",
+        true,
+        null,
+        TExampleException.class,
+        "sent msg",
+        (client, msg, throwException) -> {
+          return client.returnString(msg, throwException);
+        });
+  }
+
+  @Test
+  public void testSyncClientMustThrowExceptionReturnVoidThrows() throws Exception {
+    checkSyncClient("returnVoidThrows",
+        "sent msg",
+        true,
+        null,
+        TExampleException.class,
+        "sent msg",
+        (client, msg, throwException) -> {
+          client.returnVoidThrows(msg, throwException);
+          return null;
+        });
+  }
+
+  @Test
+  public void testSyncClientMustThrowExceptionReturnVoidNoThrowsRuntimeException() throws Exception {
+    checkSyncClient("returnVoidNoThrowsRuntimeException",
+        "sent msg",
+        true,
+        null,
+        TApplicationException.class,
+        serverImplementationType == ServerImplementationType.ASYNC_SERVER ? "sent msg"
+            : null, // sync server return "Internal error processing returnVoidNoThrowsRuntimeException" message
+        (client, msg, throwException) -> {
+          client.returnVoidNoThrowsRuntimeException(msg, throwException);
+          return null;
+        });
+  }
+
+  @Test
+  public void testSyncClientMustThrowExceptionReturnVoidNoThrowsTApplicationException() throws Exception {
+    checkSyncClient("returnVoidNoThrowsTApplicationException",
+        "sent msg",
+        true,
+        null,
+        TApplicationException.class,
+        "sent msg",
+        (client, msg, throwException) -> {
+          client.returnVoidNoThrowsTApplicationException(msg, throwException);
+          return null;
+        });
+  }
+
+
+  @Test
+  public void testAsyncClientMustReturnResultReturnString() throws Throwable {
+    checkAsyncClient("returnString",
+        "sent msg",
+        false,
+        "sent msg",
+        null,
+        null,
+        (client, msg, throwException, resultHandler) -> {
+          client.returnString(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientMustReturnResultReturnVoidThrows() throws Throwable {
+    checkAsyncClient("returnVoidThrows",
+        "sent msg",
+        false,
+        (Void) null,
+        null,
+        null,
+        (client, msg, throwException, resultHandler) -> {
+          client.returnVoidThrows(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientMustReturnResultReturnVoidNoThrowsRuntimeException() throws Throwable {
+    checkAsyncClient("returnVoidNoThrowsRuntimeException",
+        "sent msg",
+        false,
+        (Void) null,
+        null,
+        null,
+        (client, msg, throwException, resultHandler) -> {
+          client.returnVoidNoThrowsRuntimeException(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientMustReturnResultReturnVoidNoThrowsTApplicationException() throws Throwable {
+    checkAsyncClient("returnVoidNoThrowsTApplicationException",
+        "sent msg",
+        false,
+        (Void) null,
+        null,
+        null,
+        (client, msg, throwException, resultHandler) -> {
+          client.returnVoidNoThrowsTApplicationException(msg, throwException, resultHandler);
+        });
+  }
+
+
+  @Test
+  public void testAsyncClientMustThrowExceptionReturnString() throws Throwable {
+    checkAsyncClient("returnString",
+        "sent msg",
+        true,
+        (String) null,
+        TExampleException.class,
+        "sent msg",
+        (client, msg, throwException, resultHandler) -> {
+          client.returnString(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientMustThrowExceptionReturnVoidThrows() throws Throwable {
+    checkAsyncClient("returnVoidThrows",
+        "sent msg",
+        true,
+        (Void) null,
+        TExampleException.class,
+        "sent msg",
+        (client, msg, throwException, resultHandler) -> {
+          client.returnVoidThrows(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientMustThrowExceptionReturnVoidNoThrowsRuntimeException() throws Throwable {
+    checkAsyncClient("returnVoidNoThrowsRuntimeException",
+        "sent msg",
+        true,
+        (Void) null,
+        TApplicationException.class,
+        serverImplementationType == ServerImplementationType.ASYNC_SERVER ? "sent msg"
+            : null, // sync server return "Internal error processing returnVoidNoThrowsRuntimeException" message
+        (client, msg, throwException, resultHandler) -> {
+          client.returnVoidNoThrowsRuntimeException(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientMustThrowExceptionReturnVoidNoThrowsTApplicationException() throws Throwable {
+    checkAsyncClient("returnVoidNoThrowsTApplicationException",
+        "sent msg",
+        true,
+        (Void) null,
+        TApplicationException.class,
+        "sent msg",
+        (client, msg, throwException, resultHandler) -> {
+          client.returnVoidNoThrowsTApplicationException(msg, throwException, resultHandler);
+        });
+  }
+
+
+  @Test
+  public void testSyncClientNoWaitForResultNoExceptionOnewayVoidNoThrows() throws Exception {
+    checkSyncClient("onewayVoidNoThrows",
+        "sent msg",
+        false,
+        null,
+        null,
+        null,
+        (client, msg, throwException) -> {
+          client.onewayVoidNoThrows(msg, throwException);
+          return null;
+        });
+  }
+
+  @Test
+  public void testSyncClientNoWaitForResultExceptionOnewayVoidNoThrows() throws Exception {
+    checkSyncClient("onewayVoidNoThrows",
+        "sent msg",
+        true,
+        null,
+        null,
+        null,
+        (client, msg, throwException) -> {
+          client.onewayVoidNoThrows(msg, throwException);
+          return null;
+        });
+  }
+
+  @Test
+  public void testAsyncClientNoWaitForResultNoExceptionOnewayVoidNoThrows() throws Throwable {
+    checkAsyncClient("onewayVoidNoThrows",
+        "sent msg",
+        false,
+        (Void) null,
+        null,
+        null,
+        (client, msg, throwException, resultHandler) -> {
+          client.onewayVoidNoThrows(msg, throwException, resultHandler);
+        });
+  }
+
+  @Test
+  public void testAsyncClientNoWaitForResultExceptionOnewayVoidNoThrows() throws Throwable {
+    checkAsyncClient("onewayVoidNoThrows",
+        "sent msg",
+        true,
+        (Void) null,
+        null,
+        null,
+        (client, msg, throwException, resultHandler) -> {
+          client.onewayVoidNoThrows(msg, throwException, resultHandler);
+        });
+  }
+
+
+  private void checkSyncClient(String desc,
+      String msg,
+      boolean throwException,
+      String expectedResult,
+      Class<?> expectedExceptionClass,
+      String expectedExceptionMsg,
+      SyncCall<TAppService01.Iface, String, Boolean, String> call) throws Exception {
+    if (log.isInfoEnabled()) {
+      log.info("start test checkSyncClient::" + desc + ", throwException: " + throwException
+          + ", serverImplementationType: "
+          + serverImplementationType);
+    }
+    Assert.assertNotEquals(-1, serverPort);
+    try (TTransport clientTransport = new TFramedTransport(new TSocket(new TConfiguration(),
+        "localhost",
+        serverPort,
+        TIMEOUT_MILLIS))) {
+      clientTransport.open();
+      TAppService01.Iface client = new TAppService01.Client(new TBinaryProtocol(clientTransport));
+
+      try {
+
+        String result = call.apply(client, msg, throwException);
+
+        if (throwException && expectedExceptionClass != null) {
+          Assert.fail("No exception, but must!!!");
+        } else {
+          // expected
+          Assert.assertEquals(expectedResult, result);
+        }
+      } catch (TExampleException | TApplicationException x) {
+        if (log.isInfoEnabled()) {
+          log.info("Exception: " + x, x);
+        }
+        if (throwException) {
+          // expected
+          Assert.assertEquals(expectedExceptionClass, x.getClass());
+          if (expectedExceptionMsg != null) {
+            Assert.assertEquals(expectedExceptionMsg, x.getMessage());
+          }
+        } else {
+          Assert.fail();
+        }
+      }
+    }
+  }
+
+  private <T> void checkAsyncClient(String desc,
+      String msg,
+      boolean throwException,
+      T expectedResult,
+      Class<?> expectedExceptionClass,
+      String expectedExceptionMsg,
+      AsyncCall<TAppService01.AsyncClient, String, Boolean, AsyncMethodCallback<T>> call) throws Throwable {
+    if (log.isInfoEnabled()) {
+      log.info("start test checkAsyncClient::" + desc + ", throwException: " + throwException
+          + ", serverImplementationType: "
+          + serverImplementationType);
+    }
+    Assert.assertNotEquals(serverPort, -1);
+    try (TNonblockingSocket clientTransportAsync = new TNonblockingSocket("localhost", serverPort, TIMEOUT_MILLIS)) {
+      TAsyncClientManager asyncClientManager = new TAsyncClientManager();
+      try {
+        TAppService01.AsyncClient asyncClient = new TAppService01.AsyncClient(new TBinaryProtocol.Factory(),
+            asyncClientManager,
+            clientTransportAsync);
+        asyncClient.setTimeout(TIMEOUT_MILLIS);
+
+        CompletableFuture<T> futureResult = new CompletableFuture<>();
+
+        call.apply(asyncClient, msg, throwException, new AsyncMethodCallback<T>() {
+
+          @Override
+          public void onError(Exception exception) {
+            futureResult.completeExceptionally(exception);
+          }
+
+          @Override
+          public void onComplete(T response) {
+            futureResult.complete(response);
+          }
+
+        });
+
+        try {
+          T result;
+          try {
+            result = futureResult.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+          } catch (ExecutionException x) {
+            throw x.getCause();
+          }
+
+          if (throwException && expectedExceptionClass != null) {
+            Assert.fail("No exception, but must!!!");
+          } else {
+            // expected
+            Assert.assertEquals(expectedResult, result);
+          }
+        } catch (TExampleException | TApplicationException x) {
+          if (log.isInfoEnabled()) {
+            log.info("Exception: " + x, x);
+          }
+          if (throwException) {
+            // expected
+            Assert.assertEquals(expectedExceptionClass, x.getClass());
+            if (expectedExceptionMsg != null) {
+              Assert.assertEquals(expectedExceptionMsg, x.getMessage());
+            }
+          } else {
+            Assert.fail();
+          }
+        }
+      } finally {
+        asyncClientManager.stop();
+      }
+    }
+  }
+
+
+  private enum ServerImplementationType {
+
+    SYNC_SERVER(() -> {
+      ServiceSyncImp service = new ServiceSyncImp();
+      return Pair.of(new TAppService01.Processor<>(service), service);
+    }),
+    ASYNC_SERVER(() -> {
+      ServiceAsyncImp service = new ServiceAsyncImp();
+      return Pair.of(new TAppService01.AsyncProcessor<>(service), service);
+    });
+
+    final TProcessor processor;
+    final ServiceBase service;
+
+    ServerImplementationType(Supplier<Pair<TProcessor, ServiceBase>> supplier) {
+      Pair<TProcessor, ServiceBase> pair = supplier.get();
+      this.processor = pair.getLeft();
+      this.service = pair.getRight();
+    }
+  }
+
+
+  @FunctionalInterface
+  private interface SyncCall<T, U, V, R> {
+
+    R apply(T t, U u, V v) throws Exception;
+
+  }
+
+
+  @FunctionalInterface
+  private interface AsyncCall<T, U, V, X> {
+
+    void apply(T t, U u, V v, X x) throws Exception;
+
+  }
+
+}
diff --git a/test/Makefile.am b/test/Makefile.am
index 2199f1e..6bf12b8 100755
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -167,6 +167,7 @@ EXTRA_DIST = \
 	UnsafeTypes.thrift \
 	Service.thrift \
 	SpecificNameTest.thrift \
+	VoidMethExceptionsTest.thrift \
 	partial/thrift_test_schema.thrift \
 	known_failures_Linux.json \
 	test.py \
diff --git a/test/VoidMethExceptionsTest.thrift b/test/VoidMethExceptionsTest.thrift
new file mode 100644
index 0000000..fc75976
--- /dev/null
+++ b/test/VoidMethExceptionsTest.thrift
@@ -0,0 +1,13 @@
+namespace java thrift.test.voidmethexceptions
+
+exception TExampleException {
+  1: required string message;
+}
+
+service TAppService01 {
+  string returnString(1: string msg, 2: bool throwException) throws (1:TExampleException error);
+  void returnVoidThrows(1: string msg, 2: bool throwException) throws (1:TExampleException error);
+  void returnVoidNoThrowsRuntimeException(1: string msg, 2: bool throwException);
+  void returnVoidNoThrowsTApplicationException(1: string msg, 2: bool throwException);
+  oneway void onewayVoidNoThrows(1: string msg, 2: bool throwException);
+}