Introduce CallFailedRuntimeException in client
diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/CallFailedRuntimeException.java b/fineract-client/src/main/java/org/apache/fineract/client/util/CallFailedRuntimeException.java
new file mode 100644
index 0000000..6717394
--- /dev/null
+++ b/fineract-client/src/main/java/org/apache/fineract/client/util/CallFailedRuntimeException.java
@@ -0,0 +1,73 @@
+/**
+ * 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.fineract.client.util;
+
+import java.io.IOException;
+import retrofit2.Call;
+import retrofit2.Response;
+
+/**
+ * Exception thrown by {@link Calls#ok(Call)} when {@link Call}s fail.
+ *
+ * @author Michael Vorburger.ch
+ */
+public class CallFailedRuntimeException extends RuntimeException {
+
+    private final Call<?> call;
+    private final Response<?> response;
+
+    public <T> CallFailedRuntimeException(Call<T> call, Throwable t) {
+        super("HTTP failed: " + call.request().toString(), t);
+        this.call = call;
+        this.response = null;
+    }
+
+    public <T> CallFailedRuntimeException(Call<T> call, Response<T> response) {
+        super(message(call, response));
+        this.call = call;
+        this.response = response;
+    }
+
+    private static String message(Call<?> call, Response<?> response) {
+        StringBuilder sb = new StringBuilder("HTTP failed: " + call.request() + "; " + response);
+        if (response.message() != null && !response.message().isEmpty()) {
+            sb.append("; message: " + response.message());
+        }
+        String errorBody;
+        try {
+            errorBody = response.errorBody().string();
+            if (errorBody != null) {
+                sb.append("; errorBody: " + errorBody);
+            }
+        } catch (IOException e) {
+            // Ignore.
+        }
+        return sb.toString();
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> Call<T> getCall() {
+        return (Call<T>) call;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> Response<T> getResponse() {
+        return (Response<T>) response;
+    }
+}
diff --git a/fineract-client/src/main/java/org/apache/fineract/client/util/Calls.java b/fineract-client/src/main/java/org/apache/fineract/client/util/Calls.java
index 7a4f69f..c07cddd 100644
--- a/fineract-client/src/main/java/org/apache/fineract/client/util/Calls.java
+++ b/fineract-client/src/main/java/org/apache/fineract/client/util/Calls.java
@@ -37,15 +37,20 @@
      * @param call
      *            the Call to execute
      * @return the body of the successful call (never null)
-     * @throws IOException
+     * @throws CallFailedRuntimeException
      *             thrown either if a problem occurred talking to the server, or the HTTP response code was not
      *             [200..300) successful
      */
-    public static <T> T ok(Call<T> call) throws IOException {
-        Response<T> response = call.execute();
+    public static <T> T ok(Call<T> call) throws CallFailedRuntimeException {
+        Response<T> response;
+        try {
+            response = call.execute();
+        } catch (IOException e) {
+            throw new CallFailedRuntimeException(call, e);
+        }
         if (response.isSuccessful()) {
             return response.body();
         }
-        throw new IOException("HTTP failed: " + call.request() + "; " + response);
+        throw new CallFailedRuntimeException(call, response);
     }
 }
diff --git a/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientTest.java b/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientTest.java
index 0c501c6..9374c62 100644
--- a/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientTest.java
+++ b/fineract-client/src/test/java/org/apache/fineract/client/test/FineractClientTest.java
@@ -22,7 +22,6 @@
 import static org.apache.fineract.client.util.Calls.ok;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
-import java.io.IOException;
 import org.apache.fineract.client.util.FineractClient;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -34,13 +33,13 @@
  */
 public class FineractClientTest {
 
-    void checkClients(FineractClient fineract) throws IOException {
+    void checkClients(FineractClient fineract) {
         assertThat(ok(fineract.clients.retrieveAll20(null, null, null, null, null, null, null, null, 0, 100, null, null, null))
                 .getTotalFilteredRecords()).isAtLeast(3);
     }
 
     @Test
-    void testRetrieveAllClientsFromFineractDev() throws IOException {
+    void testRetrieveAllClientsFromFineractDev() {
         FineractClient fineract = FineractClient.builder().baseURL("https://demo.fineract.dev/fineract-provider/api/v1/").tenant("default")
                 .basicAuth("mifos", "password").build();
         checkClients(fineract);
@@ -48,7 +47,7 @@
 
     @Test
     @Disabled // TODO remove Disabled once https://issues.apache.org/jira/browse/FINERACT-1209 is fixed
-    void testRetrieveAllClientsFromLocalhostWithInsecureSelfSignedCert() throws IOException {
+    void testRetrieveAllClientsFromLocalhostWithInsecureSelfSignedCert() {
         FineractClient fineract = FineractClient.builder().baseURL("https://localhost:8443/fineract-provider/api/v1/").tenant("default")
                 .basicAuth("mifos", "password").insecure(true).build();
         checkClients(fineract);
@@ -56,14 +55,14 @@
 
     @Test
     @Disabled // TODO remove Ignore once https://issues.apache.org/jira/browse/FINERACT-1221 is fixed
-    void testInvalidOperations() throws IOException {
+    void testInvalidOperations() {
         FineractClient.Builder builder = FineractClient.builder().baseURL("http://test/").tenant("default").basicAuth("mifos", "password");
         builder.getApiClient().getAdapterBuilder().validateEagerly(true); // see FINERACT-1221
         builder.build();
     }
 
     @Test
-    void testFineractClientBuilder() throws IOException {
+    void testFineractClientBuilder() {
         assertThrows(IllegalStateException.class, () -> {
             FineractClient.builder().build();
         });