KNOX-2912 - Don't fail over non idempotent requests unless it's a connect exception (#757)

diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java
index 826275b..8d5f5b8 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatch.java
@@ -17,6 +17,8 @@
  */
 package org.apache.knox.gateway.ha.dispatch;
 
+import static org.apache.knox.gateway.util.HttpUtils.isConnectionError;
+
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.http.HttpResponse;
@@ -216,8 +218,8 @@
       inboundResponse = executeOutboundRequest(outboundRequest);
       writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse);
     } catch ( IOException e ) {
-      /* if non-idempotent requests are not allowed to failover */
-      if(!failoverNonIdempotentRequestEnabled && nonIdempotentRequests.stream().anyMatch(outboundRequest.getMethod()::equalsIgnoreCase)) {
+      /* if non-idempotent requests are not allowed to failover, unless it's a connection error */
+      if(!isConnectionError(e.getCause()) && isNonIdempotentAndNonIdempotentFailoverDisabled(outboundRequest)) {
         LOG.cannotFailoverNonIdempotentRequest(outboundRequest.getMethod(), e.toString());
         /* mark endpoint as failed */
         markEndpointFailed(outboundRequest, inboundRequest);
@@ -229,6 +231,10 @@
     }
   }
 
+  private boolean isNonIdempotentAndNonIdempotentFailoverDisabled(HttpUriRequest outboundRequest) {
+    return !failoverNonIdempotentRequestEnabled && nonIdempotentRequests.stream().anyMatch(outboundRequest.getMethod()::equalsIgnoreCase);
+  }
+
   private Optional<URI> setBackendfromHaCookie(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest) {
       if (loadBalancingEnabled && stickySessionsEnabled && inboundRequest.getCookies() != null) {
           for (Cookie cookie : inboundRequest.getCookies()) {
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java
index 004af14..901d87b 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/i18n/HaDispatchMessages.java
@@ -54,6 +54,6 @@
   @Message(level = MessageLevel.ERROR, text = "Unsupported encoding, cause: {0}")
   void unsupportedEncodingException(String cause);
 
-  @Message(level = MessageLevel.ERROR, text = "Request is non-idempotent {0}, failover prevented, to allow non-idempotent requests to failover set 'failoverNonIdempotentRequestEnabled=true' in HA config. Cause {1}")
+  @Message(level = MessageLevel.ERROR, text = "Request is non-idempotent {0}, failover prevented, to allow non-idempotent requests to failover set 'failoverNonIdempotentRequestEnabled=true' in HA config. Non connection related error: {1}")
   void cannotFailoverNonIdempotentRequest(String method, String cause);
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java
index 066a842..58f0694 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/DefaultDispatch.java
@@ -181,7 +181,7 @@
       // We do not want to expose back end host. port end points to clients, see JIRA KNOX-58
       auditor.audit( Action.DISPATCH, outboundRequest.getURI().toString(), ResourceType.URI, ActionOutcome.FAILURE );
       LOG.dispatchServiceConnectionException( outboundRequest.getURI(), e );
-      throw new IOException( RES.dispatchConnectionError() );
+      throw new IOException(RES.dispatchConnectionError(), e);
     }
     return inboundResponse;
   }
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/HttpUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/HttpUtils.java
index ac96214..46d36f5 100644
--- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/HttpUtils.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/HttpUtils.java
@@ -17,8 +17,14 @@
  */
 package org.apache.knox.gateway.util;
 
+import static java.util.Arrays.asList;
+
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.net.NoRouteToHostException;
+import java.net.SocketException;
 import java.net.URLDecoder;
+import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -28,6 +34,9 @@
 import java.util.StringTokenizer;
 
 public class HttpUtils {
+  private static final List<Class<? extends IOException>> connectionErrors = asList(UnknownHostException.class, NoRouteToHostException.class,
+          SocketException.class);
+
   public static Map<String, List<String>> splitQuery(String queryString)
       throws UnsupportedEncodingException {
     final Map<String, List<String>> queryPairs = new LinkedHashMap<>();
@@ -117,4 +126,22 @@
     }
     map.put( name, values );
   }
+
+  /**
+   * Determine if the specified exception represents an error condition that is related to a connection error.
+   *
+   * @return true, if the exception represents an connection error; otherwise, false.
+   */
+  public static boolean isConnectionError(Throwable exception) {
+    boolean isFailoverException = false;
+    if (exception != null) {
+      for (Class<? extends Exception> exceptionType : connectionErrors) {
+        if (exceptionType.isAssignableFrom(exception.getClass())) {
+          isFailoverException = true;
+          break;
+        }
+      }
+    }
+    return isFailoverException;
+  }
 }
diff --git a/gateway-util-common/src/test/java/org/apache/knox/gateway/util/HttpUtilsTest.java b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/HttpUtilsTest.java
index 05aea58..7a73095 100644
--- a/gateway-util-common/src/test/java/org/apache/knox/gateway/util/HttpUtilsTest.java
+++ b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/HttpUtilsTest.java
@@ -19,11 +19,17 @@
 
 import org.junit.Test;
 
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.NoRouteToHostException;
+import java.net.PortUnreachableException;
+import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 
+import static org.apache.knox.gateway.util.HttpUtils.isConnectionError;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -212,4 +218,14 @@
     assertThat( map.get( "qry" ).size(), is( 1 ) );
     assertThat( map.get( "qry" ).get(0), is( "Hadoop:service=NameNode,name=NameNodeInfo" ) );
   }
+
+  @Test
+  public void testRelevantConnectionErrors() {
+    assertThat(isConnectionError(new UnknownHostException()), is(true));
+    assertThat(isConnectionError(new ConnectException()), is(true));
+    assertThat(isConnectionError(new NoRouteToHostException()), is(true));
+    assertThat(isConnectionError(new PortUnreachableException()), is(true));
+    assertThat(isConnectionError(new IOException()), is(false));
+    assertThat(isConnectionError(new RuntimeException()), is(false));
+  }
 }