KNOX-2890 - Prevent non-idempotent requests from failing over (#742)

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 d731390..e42ab96 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
@@ -61,18 +61,20 @@
 
   protected static final HaDispatchMessages LOG = MessagesFactory.get(HaDispatchMessages.class);
 
-  private int maxFailoverAttempts = HaServiceConfigConstants.DEFAULT_MAX_FAILOVER_ATTEMPTS;
+  protected int maxFailoverAttempts = HaServiceConfigConstants.DEFAULT_MAX_FAILOVER_ATTEMPTS;
 
-  private int failoverSleep = HaServiceConfigConstants.DEFAULT_FAILOVER_SLEEP;
+  protected int failoverSleep = HaServiceConfigConstants.DEFAULT_FAILOVER_SLEEP;
 
-  private HaProvider haProvider;
+  protected HaProvider haProvider;
 
   private static final Map<String, String> urlToHashLookup = new HashMap<>();
   private static final Map<String, String> hashToUrlLookup = new HashMap<>();
+  protected static final List<String> nonIdempotentRequests = Arrays.asList("POST", "PATCH", "CONNECT");
 
   private boolean loadBalancingEnabled = HaServiceConfigConstants.DEFAULT_LOAD_BALANCING_ENABLED;
   private boolean stickySessionsEnabled = HaServiceConfigConstants.DEFAULT_STICKY_SESSIONS_ENABLED;
   private boolean noFallbackEnabled = HaServiceConfigConstants.DEFAULT_NO_FALLBACK_ENABLED;
+  protected boolean failoverNonIdempotentRequestEnabled = HaServiceConfigConstants.DEFAULT_FAILOVER_NON_IDEMPOTENT;
   private String stickySessionCookieName = HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME;
   private List<String> disableLoadBalancingForUserAgents = Arrays.asList(HaServiceConfigConstants.DEFAULT_DISABLE_LB_USER_AGENTS);
 
@@ -97,6 +99,7 @@
       maxFailoverAttempts = serviceConfig.getMaxFailoverAttempts();
       failoverSleep = serviceConfig.getFailoverSleep();
       loadBalancingEnabled = serviceConfig.isLoadBalancingEnabled();
+      failoverNonIdempotentRequestEnabled = serviceConfig.isFailoverNonIdempotentRequestEnabled();
 
       /* enforce dependency */
       stickySessionsEnabled = loadBalancingEnabled && serviceConfig.isStickySessionEnabled();
@@ -213,8 +216,14 @@
       inboundResponse = executeOutboundRequest(outboundRequest);
       writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse);
     } catch ( IOException e ) {
-      LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
-      failoverRequest(outboundRequest, inboundRequest, outboundResponse, inboundResponse, e);
+      /* if non-idempotent requests are not allowed to failover */
+      if(!failoverNonIdempotentRequestEnabled && nonIdempotentRequests.stream().anyMatch(outboundRequest.getMethod()::equalsIgnoreCase)) {
+        LOG.cannotFailoverNonIdempotentRequest(outboundRequest.getMethod(), e.toString());
+        throw e;
+      } else {
+        LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
+        failoverRequest(outboundRequest, inboundRequest, outboundResponse, inboundResponse, e);
+      }
     }
   }
 
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 8aaa27d..004af14 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
@@ -53,4 +53,7 @@
 
   @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}")
+  void cannotFailoverNonIdempotentRequest(String method, String cause);
 }
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java
index 1a5706b..801d312 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/HaServiceConfig.java
@@ -62,4 +62,9 @@
   void setDisableStickySessionForUserAgents(String disableStickySessionForUserAgents);
 
   String getStickySessionDisabledUserAgents();
+
+  boolean isFailoverNonIdempotentRequestEnabled();
+
+  void setFailoverNonIdempotentRequestEnabled(boolean failoverNonIdempotentRequestEnabled);
+
 }
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java
index 5a23006..baa08c3 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/DefaultHaServiceConfig.java
@@ -35,6 +35,8 @@
 
   private boolean isNoFallbackEnabled = DEFAULT_NO_FALLBACK_ENABLED;
 
+  private boolean failoverNonIdempotentRequestEnabled = DEFAULT_FAILOVER_NON_IDEMPOTENT;
+
   private String stickySessionCookieName = DEFAULT_STICKY_SESSION_COOKIE_NAME;
 
   private String zookeeperEnsemble;
@@ -156,4 +158,15 @@
   public String getStickySessionDisabledUserAgents() {
     return disableStickySessionForUserAgents;
   }
+
+  @Override
+  public boolean isFailoverNonIdempotentRequestEnabled() {
+    return failoverNonIdempotentRequestEnabled;
+  }
+
+  @Override
+  public void setFailoverNonIdempotentRequestEnabled(
+      boolean failoverNonIdempotentRequestEnabled) {
+    this.failoverNonIdempotentRequestEnabled = failoverNonIdempotentRequestEnabled;
+  }
 }
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java
index 21edd17..05d813e 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorConstants.java
@@ -53,4 +53,12 @@
    String STICKY_SESSION_COOKIE_NAME = "stickySessionCookieName";
 
    String DISABLE_LB_USER_AGENTS = "disableLoadBalancingForUserAgents";
+
+   /**
+    * For non-idempotent requests such as
+    * POST, PATCH, CONNECT
+    * should we failover?
+    * default is false (no).
+    */
+   String FAILOVER_NON_IDEMPOTENT = "failoverNonIdempotentRequestEnabled";
 }
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java
index 88d927b..bf5886f 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorFactory.java
@@ -42,10 +42,62 @@
     final boolean loadBalancingEnabled = Boolean.parseBoolean(configMap.getOrDefault(CONFIG_LOAD_BALANCING_ENABLED, Boolean.toString(DEFAULT_LOAD_BALANCING_ENABLED)));
     final boolean noFallbackEnabled = Boolean.parseBoolean(configMap.getOrDefault(CONFIG_NO_FALLBACK_ENABLED, Boolean.toString(DEFAULT_NO_FALLBACK_ENABLED)));
     final String stickySessionCookieName = configMap.getOrDefault(STICKY_SESSION_COOKIE_NAME, DEFAULT_STICKY_SESSION_COOKIE_NAME);
-
+    final boolean failoverNonIdempotentRequestEnabled = Boolean.parseBoolean(configMap.getOrDefault(FAILOVER_NON_IDEMPOTENT, Boolean.toString(DEFAULT_FAILOVER_NON_IDEMPOTENT)));
     final String disableLoadBalancingForUserAgentsConfig = configMap.getOrDefault(DISABLE_LB_USER_AGENTS, DEFAULT_DISABLE_LB_USER_AGENTS);
     return createServiceConfig(serviceName, enabled, maxFailoverAttempts, failoverSleep, zookeeperEnsemble, zookeeperNamespace, stickySessionsEnabled, loadBalancingEnabled,
-            stickySessionCookieName, noFallbackEnabled, disableLoadBalancingForUserAgentsConfig);
+            stickySessionCookieName, noFallbackEnabled, disableLoadBalancingForUserAgentsConfig, failoverNonIdempotentRequestEnabled);
+  }
+
+  public static HaServiceConfig createServiceConfig(String serviceName, String enabledValue,
+      String maxFailoverAttemptsValue, String failoverSleepValue,
+      String zookeeperEnsemble, String zookeeperNamespace,
+      String loadBalancingEnabledValue, String stickySessionsEnabledValue,
+      String stickySessionCookieNameValue, String noFallbackEnabledValue,
+      String disableLoadBalancingForUserAgentsValue, String failoverNonIdempotentRequestEnabledValue) {
+
+    boolean enabled = DEFAULT_ENABLED;
+    int maxFailoverAttempts = DEFAULT_MAX_FAILOVER_ATTEMPTS;
+    int failoverSleep = DEFAULT_FAILOVER_SLEEP;
+    boolean stickySessionsEnabled = DEFAULT_STICKY_SESSIONS_ENABLED;
+    boolean loadBalancingEnabled = DEFAULT_LOAD_BALANCING_ENABLED;
+    boolean noFallbackEnabled = DEFAULT_NO_FALLBACK_ENABLED;
+    String stickySessionCookieName = DEFAULT_STICKY_SESSION_COOKIE_NAME;
+    String disableLoadBalancingForUserAgentsConfig = DEFAULT_DISABLE_LB_USER_AGENTS;
+    boolean failoverNonIdempotentRequestEnabled = DEFAULT_FAILOVER_NON_IDEMPOTENT;
+
+    if (enabledValue != null && !enabledValue.trim().isEmpty()) {
+      enabled = Boolean.parseBoolean(enabledValue);
+    }
+    if (maxFailoverAttemptsValue != null && !maxFailoverAttemptsValue.trim().isEmpty()) {
+      maxFailoverAttempts = Integer.parseInt(maxFailoverAttemptsValue);
+    }
+    if (failoverSleepValue != null && !failoverSleepValue.trim().isEmpty()) {
+      failoverSleep = Integer.parseInt(failoverSleepValue);
+    }
+    if (stickySessionsEnabledValue != null && !stickySessionsEnabledValue.trim().isEmpty()) {
+      stickySessionsEnabled = Boolean.parseBoolean(stickySessionsEnabledValue);
+    }
+    if (loadBalancingEnabledValue != null && !loadBalancingEnabledValue.trim().isEmpty()) {
+      loadBalancingEnabled = Boolean.parseBoolean(loadBalancingEnabledValue);
+    }
+    if (stickySessionCookieNameValue != null && !stickySessionCookieNameValue.trim().isEmpty()) {
+      stickySessionCookieName = stickySessionCookieNameValue;
+    }
+    if (noFallbackEnabledValue != null && !noFallbackEnabledValue.trim().isEmpty()) {
+      noFallbackEnabled = Boolean.parseBoolean(noFallbackEnabledValue);
+    }
+    if(StringUtils.isNotBlank(disableLoadBalancingForUserAgentsValue)) {
+      disableLoadBalancingForUserAgentsConfig = disableLoadBalancingForUserAgentsValue;
+    }
+
+    if (failoverNonIdempotentRequestEnabledValue != null && !failoverNonIdempotentRequestEnabledValue.trim().isEmpty()) {
+      failoverNonIdempotentRequestEnabled = Boolean.parseBoolean(failoverNonIdempotentRequestEnabledValue);
+    }
+
+    return createServiceConfig(serviceName, enabled, maxFailoverAttempts, failoverSleep, zookeeperEnsemble, zookeeperNamespace, stickySessionsEnabled, loadBalancingEnabled,
+        stickySessionCookieName, noFallbackEnabled, disableLoadBalancingForUserAgentsConfig, failoverNonIdempotentRequestEnabled);
+
+
   }
 
   /**
@@ -70,49 +122,18 @@
                                                      String loadBalancingEnabledValue, String stickySessionsEnabledValue,
                                                      String stickySessionCookieNameValue, String noFallbackEnabledValue,
                                                      String disableLoadBalancingForUserAgentsValue) {
-      boolean enabled = DEFAULT_ENABLED;
-      int maxFailoverAttempts = DEFAULT_MAX_FAILOVER_ATTEMPTS;
-      int failoverSleep = DEFAULT_FAILOVER_SLEEP;
-      boolean stickySessionsEnabled = DEFAULT_STICKY_SESSIONS_ENABLED;
-      boolean loadBalancingEnabled = DEFAULT_LOAD_BALANCING_ENABLED;
-      boolean noFallbackEnabled = DEFAULT_NO_FALLBACK_ENABLED;
-      String stickySessionCookieName = DEFAULT_STICKY_SESSION_COOKIE_NAME;
-      String disableLoadBalancingForUserAgentsConfig = DEFAULT_DISABLE_LB_USER_AGENTS;
-      if (enabledValue != null && !enabledValue.trim().isEmpty()) {
-         enabled = Boolean.parseBoolean(enabledValue);
-      }
-      if (maxFailoverAttemptsValue != null && !maxFailoverAttemptsValue.trim().isEmpty()) {
-         maxFailoverAttempts = Integer.parseInt(maxFailoverAttemptsValue);
-      }
-      if (failoverSleepValue != null && !failoverSleepValue.trim().isEmpty()) {
-         failoverSleep = Integer.parseInt(failoverSleepValue);
-      }
-      if (stickySessionsEnabledValue != null && !stickySessionsEnabledValue.trim().isEmpty()) {
-         stickySessionsEnabled = Boolean.parseBoolean(stickySessionsEnabledValue);
-      }
-      if (loadBalancingEnabledValue != null && !loadBalancingEnabledValue.trim().isEmpty()) {
-         loadBalancingEnabled = Boolean.parseBoolean(loadBalancingEnabledValue);
-      }
-      if (stickySessionCookieNameValue != null && !stickySessionCookieNameValue.trim().isEmpty()) {
-         stickySessionCookieName = stickySessionCookieNameValue;
-      }
-      if (noFallbackEnabledValue != null && !noFallbackEnabledValue.trim().isEmpty()) {
-         noFallbackEnabled = Boolean.parseBoolean(noFallbackEnabledValue);
-      }
-      if(StringUtils.isNotBlank(disableLoadBalancingForUserAgentsValue)) {
-        disableLoadBalancingForUserAgentsConfig = disableLoadBalancingForUserAgentsValue;
-      }
 
-     return createServiceConfig(serviceName, enabled, maxFailoverAttempts, failoverSleep, zookeeperEnsemble, zookeeperNamespace, stickySessionsEnabled, loadBalancingEnabled,
-             stickySessionCookieName, noFallbackEnabled, disableLoadBalancingForUserAgentsConfig);
+     return createServiceConfig(serviceName, enabledValue, maxFailoverAttemptsValue, failoverSleepValue, zookeeperEnsemble, zookeeperNamespace, loadBalancingEnabledValue, stickySessionsEnabledValue,
+         stickySessionCookieNameValue, noFallbackEnabledValue, disableLoadBalancingForUserAgentsValue, Boolean.toString(DEFAULT_FAILOVER_NON_IDEMPOTENT));
    }
 
-  private static DefaultHaServiceConfig createServiceConfig(final String serviceName, final boolean enabled,
+  public static DefaultHaServiceConfig createServiceConfig(final String serviceName, final boolean enabled,
           final int maxFailoverAttempts, final int failoverSleepValue,
           final String zookeeperEnsemble, final String zookeeperNamespace,
           final boolean stickySessionsEnabled, final boolean loadBalancingEnabled,
           final String stickySessionCookieName,
-          final boolean noFallbackEnabled, final String disableStickySessionForUserAgents) {
+          final boolean noFallbackEnabled, final String disableStickySessionForUserAgents,
+          final boolean failoverNonIdempotentRequestEnabled) {
     DefaultHaServiceConfig serviceConfig = new DefaultHaServiceConfig(serviceName);
     serviceConfig.setEnabled(enabled);
     serviceConfig.setMaxFailoverAttempts(maxFailoverAttempts);
@@ -124,6 +145,7 @@
     serviceConfig.setStickySessionCookieName(stickySessionCookieName);
     serviceConfig.setNoFallbackEnabled(noFallbackEnabled);
     serviceConfig.setDisableStickySessionForUserAgents(disableStickySessionForUserAgents);
+    serviceConfig.setFailoverNonIdempotentRequestEnabled(failoverNonIdempotentRequestEnabled);
     return serviceConfig;
   }
 
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java
index b7ed878..b24f758 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaDescriptorManager.java
@@ -69,6 +69,7 @@
                if(config.getStickySessionDisabledUserAgents() != null && !config.getStickySessionDisabledUserAgents().isEmpty()) {
                   serviceElement.setAttribute(DISABLE_LB_USER_AGENTS, config.getStickySessionDisabledUserAgents());
                }
+               serviceElement.setAttribute(FAILOVER_NON_IDEMPOTENT, Boolean.toString(config.isFailoverNonIdempotentRequestEnabled()));
                root.appendChild(serviceElement);
             }
          }
@@ -99,7 +100,8 @@
                      element.getAttribute(ENABLE_STICKY_SESSIONS),
                      element.getAttribute(STICKY_SESSION_COOKIE_NAME),
                      element.getAttribute(ENABLE_NO_FALLBACK),
-                     element.getAttribute(DISABLE_LB_USER_AGENTS));
+                     element.getAttribute(DISABLE_LB_USER_AGENTS),
+                     element.getAttribute(FAILOVER_NON_IDEMPOTENT));
                descriptor.addServiceConfig(config);
             }
          }
diff --git a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java
index 092ce0a..21149c5 100644
--- a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java
+++ b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/provider/impl/HaServiceConfigConstants.java
@@ -41,6 +41,14 @@
    String STICKY_SESSION_COOKIE_NAME = "stickySessionCookieName";
 
    /**
+    * For non-idempotent requests such as
+    * POST, PATCH, CONNECT
+    * should we failover?
+    * default is false (no).
+    */
+   String FAILOVER_NON_IDEMPOTENT = "failoverNonIdempotentRequestEnabled";
+
+   /**
     * Disable loadbalancing feature based on user agent.
     * The code will look for "contains" match
     */
@@ -58,6 +66,8 @@
 
    boolean DEFAULT_NO_FALLBACK_ENABLED = false;
 
+   boolean DEFAULT_FAILOVER_NON_IDEMPOTENT = false;
+
    String DEFAULT_STICKY_SESSION_COOKIE_NAME = "KNOX_BACKEND";
 
    String DEFAULT_DISABLE_LB_USER_AGENTS = "ClouderaODBCDriverforApacheHive";
diff --git a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java
index 1545f0a..8cf39e3 100644
--- a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java
+++ b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/DefaultHaDispatchTest.java
@@ -604,6 +604,399 @@
     doTestFailoverStickyOnFallbackOff(true, sessionCookieLast);
   }
 
+  /**
+   * Test whether non-idempotent requests are prevented from failing over
+   *
+   * KNOX-2890
+   */
+  @Test
+  public void testFailoverForNonIdempotentRequests() throws Exception {
+    Cookie [] sessionCookieFirst = new Cookie[3];
+    sessionCookieFirst[0] = new Cookie(HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME + "-" + "OOZIE",
+        "59973e253ae20de796c6ef413608ec1c80fca24310a4cbdecc0ff97aeea55745");
+    sessionCookieFirst[1] = new Cookie("Test1", "Test1");
+    sessionCookieFirst[2] = new Cookie("Test2", "Test2");
+
+    final boolean enableLoadBalancing = true; // load-balancing is required for sticky sessions to be enabled
+    final boolean enableStickySession = true;
+    final boolean noFallback          = false;
+    final boolean failoverNonIdempotentRequestEnabled = false;
+
+    final String serviceName = "OOZIE";
+
+    HaDescriptor descriptor = HaDescriptorFactory.createDescriptor();
+    descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName,
+        true,
+        1,
+        1000,
+        null,
+        null,
+        enableStickySession,
+        enableLoadBalancing,
+        null,
+        noFallback,
+        null,
+        failoverNonIdempotentRequestEnabled));
+
+    final HaProvider provider = new DefaultHaProvider(descriptor);
+    final URI uri1 = new URI( "http://host1.valid" );
+    final URI uri2 = new URI( "http://host2.valid" );
+    final ArrayList<String> urlList = new ArrayList<>();
+    urlList.add(uri1.toString());
+    urlList.add(uri2.toString());
+    provider.addHaService(serviceName, urlList);
+    FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class);
+    ServletContext servletContext = EasyMock.createNiceMock(ServletContext.class);
+
+    EasyMock.expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes();
+    EasyMock.expect(servletContext.getAttribute(HaServletContextListener.PROVIDER_ATTRIBUTE_NAME)).andReturn(provider).anyTimes();
+
+    BasicHttpParams params = new BasicHttpParams();
+
+    HttpUriRequest outboundRequest = EasyMock.createNiceMock(HttpRequestBase.class);
+    EasyMock.expect(outboundRequest.getMethod()).andReturn( "POST" ).anyTimes();
+    EasyMock.expect(outboundRequest.getURI()).andReturn( uri1  ).anyTimes();
+    EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes();
+    // Capture the last request URI to be set on the request
+    Capture<URI> requestURICapture = EasyMock.newCapture(CaptureType.LAST);
+    ((HttpRequestBase) outboundRequest).setURI(EasyMock.capture(requestURICapture));
+    EasyMock.expectLastCall().anyTimes();
+
+    /* backend request */
+    HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once();
+    EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once();
+    EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once();
+
+
+    /* backend response */
+    CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class);
+    final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class);
+    final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class);
+    final Header header = EasyMock.createNiceMock(Header.class);
+    final ServletContext context = EasyMock.createNiceMock(ServletContext.class);
+    final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes(StandardCharsets.UTF_8));
+
+    EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes();
+    EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes();
+    EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes();
+    EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes();
+    EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes();
+    EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes();
+    EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes();
+    EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes();
+    EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes();
+    EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes();
+
+    HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class);
+    // Capture the status code when it is set on the response
+    Capture<Integer> statusCodeCapture = EasyMock.newCapture(CaptureType.FIRST);
+
+    outboundResponse.setStatus(EasyMock.captureInt(statusCodeCapture));
+
+    EasyMock.expectLastCall().once();
+    EasyMock.expect(outboundResponse.getOutputStream())
+        .andAnswer((IAnswer<SynchronousServletOutputStreamAdapter>) () -> new SynchronousServletOutputStreamAdapter() {
+          @Override
+          public void write( int b ) throws IOException {
+            throw new IOException( "unreachable-host" ); // Fail-over condition
+          }
+        }).once();
+
+    CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class);
+    EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes();
+
+    EasyMock.replay(filterConfig,
+        servletContext,
+        outboundRequest,
+        inboundRequest,
+        outboundResponse,
+        mockHttpClient,
+        inboundResponse,
+        statusLine,
+        entity,
+        header,
+        context,
+        config);
+
+    Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName));
+    ConfigurableHADispatch dispatch = new ConfigurableHADispatch();
+    dispatch.setHttpClient(mockHttpClient);
+    dispatch.setHaProvider(provider);
+    dispatch.setServiceRole(serviceName);
+    dispatch.init();
+    try {
+      dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse);
+      // If the call succeeds then test failed
+      throw new Exception("Expected the request to NOT failover");
+    } catch (IOException e) {
+      Assert.assertTrue("Expected the request to NOT failover", e.toString().equalsIgnoreCase("java.io.IOException: unreachable-host"));
+    }
+
+  }
+
+  /**
+   * Test that non-idempotent requests fail over when
+   * failoverNonIdempotentRequestEnabled=true
+   *
+   * KNOX-2890
+   */
+  @Test
+  public void testFailoverWhenNonIdempotentRequestsEnabled() throws Exception {
+    Cookie [] sessionCookieFirst = new Cookie[3];
+    sessionCookieFirst[0] = new Cookie(HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME + "-" + "OOZIE",
+        "59973e253ae20de796c6ef413608ec1c80fca24310a4cbdecc0ff97aeea55745");
+    sessionCookieFirst[1] = new Cookie("Test1", "Test1");
+    sessionCookieFirst[2] = new Cookie("Test2", "Test2");
+
+    final boolean enableLoadBalancing = true; // load-balancing is required for sticky sessions to be enabled
+    final boolean enableStickySession = true;
+    final boolean noFallback          = false;
+    final boolean failoverNonIdempotentRequestEnabled = true;
+
+    final String serviceName = "OOZIE";
+
+    HaDescriptor descriptor = HaDescriptorFactory.createDescriptor();
+    descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName,
+        true,
+        1,
+        1000,
+        null,
+        null,
+        enableStickySession,
+        enableLoadBalancing,
+        null,
+        noFallback,
+        null,
+        failoverNonIdempotentRequestEnabled));
+
+    final HaProvider provider = new DefaultHaProvider(descriptor);
+    final URI uri1 = new URI( "http://host1.valid" );
+    final URI uri2 = new URI( "http://host2.valid" );
+    final ArrayList<String> urlList = new ArrayList<>();
+    urlList.add(uri1.toString());
+    urlList.add(uri2.toString());
+    provider.addHaService(serviceName, urlList);
+    FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class);
+    ServletContext servletContext = EasyMock.createNiceMock(ServletContext.class);
+
+    EasyMock.expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes();
+    EasyMock.expect(servletContext.getAttribute(HaServletContextListener.PROVIDER_ATTRIBUTE_NAME)).andReturn(provider).anyTimes();
+
+    BasicHttpParams params = new BasicHttpParams();
+
+    HttpUriRequest outboundRequest = EasyMock.createNiceMock(HttpRequestBase.class);
+    EasyMock.expect(outboundRequest.getMethod()).andReturn( "POST" ).anyTimes();
+    EasyMock.expect(outboundRequest.getURI()).andReturn( uri1  ).anyTimes();
+    EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes();
+    // Capture the last request URI to be set on the request
+    Capture<URI> requestURICapture = EasyMock.newCapture(CaptureType.LAST);
+    ((HttpRequestBase) outboundRequest).setURI(EasyMock.capture(requestURICapture));
+    EasyMock.expectLastCall().anyTimes();
+
+    /* backend request */
+    HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once();
+    EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once();
+    EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once();
+
+
+    /* backend response */
+    CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class);
+    final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class);
+    final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class);
+    final Header header = EasyMock.createNiceMock(Header.class);
+    final ServletContext context = EasyMock.createNiceMock(ServletContext.class);
+    final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes(StandardCharsets.UTF_8));
+
+    EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes();
+    EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes();
+    EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes();
+    EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes();
+    EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes();
+    EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes();
+    EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes();
+    EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes();
+    EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes();
+    EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes();
+
+    HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class);
+    // Capture the status code when it is set on the response
+    Capture<Integer> statusCodeCapture = EasyMock.newCapture(CaptureType.FIRST);
+
+    outboundResponse.setStatus(EasyMock.captureInt(statusCodeCapture));
+
+    EasyMock.expectLastCall().once();
+    EasyMock.expect(outboundResponse.getOutputStream())
+        .andAnswer((IAnswer<SynchronousServletOutputStreamAdapter>) () -> new SynchronousServletOutputStreamAdapter() {
+          @Override
+          public void write( int b ) throws IOException {
+            throw new IOException( "unreachable-host" ); // Fail-over condition
+          }
+        }).once();
+
+    CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class);
+    EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes();
+
+    EasyMock.replay(filterConfig,
+        servletContext,
+        outboundRequest,
+        inboundRequest,
+        outboundResponse,
+        mockHttpClient,
+        inboundResponse,
+        statusLine,
+        entity,
+        header,
+        context,
+        config);
+
+    Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName));
+    ConfigurableHADispatch dispatch = new ConfigurableHADispatch();
+    dispatch.setHttpClient(mockHttpClient);
+    dispatch.setHaProvider(provider);
+    dispatch.setServiceRole(serviceName);
+    dispatch.init();
+
+    dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse);
+
+    Assert.assertEquals("Expected the request to have failed-over to the alternate host.", uri2, requestURICapture.getValue());
+    Assert.assertEquals("Expected the failed-over request to succeed.", HttpStatus.SC_OK, statusCodeCapture.getValue().intValue());
+
+  }
+
+  /**
+   * Test that idempotent requests (GET) can fail over.
+   *
+   * KNOX-2890
+   */
+  @Test
+  public void testFailoverForIdempotentRequest() throws Exception {
+    Cookie [] sessionCookieFirst = new Cookie[3];
+    sessionCookieFirst[0] = new Cookie(HaServiceConfigConstants.DEFAULT_STICKY_SESSION_COOKIE_NAME + "-" + "OOZIE",
+        "59973e253ae20de796c6ef413608ec1c80fca24310a4cbdecc0ff97aeea55745");
+    sessionCookieFirst[1] = new Cookie("Test1", "Test1");
+    sessionCookieFirst[2] = new Cookie("Test2", "Test2");
+
+    final boolean enableLoadBalancing = true; // load-balancing is required for sticky sessions to be enabled
+    final boolean enableStickySession = true;
+    final boolean noFallback          = false;
+    final boolean failoverNonIdempotentRequestEnabled = false;
+
+    final String serviceName = "OOZIE";
+
+    HaDescriptor descriptor = HaDescriptorFactory.createDescriptor();
+    descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName,
+        true,
+        1,
+        1000,
+        null,
+        null,
+        enableStickySession,
+        enableLoadBalancing,
+        null,
+        noFallback,
+        null,
+        failoverNonIdempotentRequestEnabled));
+
+    final HaProvider provider = new DefaultHaProvider(descriptor);
+    final URI uri1 = new URI( "http://host1.valid" );
+    final URI uri2 = new URI( "http://host2.valid" );
+    final ArrayList<String> urlList = new ArrayList<>();
+    urlList.add(uri1.toString());
+    urlList.add(uri2.toString());
+    provider.addHaService(serviceName, urlList);
+    FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class);
+    ServletContext servletContext = EasyMock.createNiceMock(ServletContext.class);
+
+    EasyMock.expect(filterConfig.getServletContext()).andReturn(servletContext).anyTimes();
+    EasyMock.expect(servletContext.getAttribute(HaServletContextListener.PROVIDER_ATTRIBUTE_NAME)).andReturn(provider).anyTimes();
+
+    BasicHttpParams params = new BasicHttpParams();
+
+    HttpUriRequest outboundRequest = EasyMock.createNiceMock(HttpRequestBase.class);
+    EasyMock.expect(outboundRequest.getMethod()).andReturn( "GET" ).anyTimes();
+    EasyMock.expect(outboundRequest.getURI()).andReturn( uri1  ).anyTimes();
+    EasyMock.expect(outboundRequest.getParams()).andReturn( params ).anyTimes();
+    // Capture the last request URI to be set on the request
+    Capture<URI> requestURICapture = EasyMock.newCapture(CaptureType.LAST);
+    ((HttpRequestBase) outboundRequest).setURI(EasyMock.capture(requestURICapture));
+    EasyMock.expectLastCall().anyTimes();
+
+    /* backend request */
+    HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new StringBuffer(uri2.toString()) ).once();
+    EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once();
+    EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once();
+
+
+    /* backend response */
+    CloseableHttpResponse inboundResponse = EasyMock.createNiceMock(CloseableHttpResponse.class);
+    final StatusLine statusLine = EasyMock.createNiceMock(StatusLine.class);
+    final HttpEntity entity = EasyMock.createNiceMock(HttpEntity.class);
+    final Header header = EasyMock.createNiceMock(Header.class);
+    final ServletContext context = EasyMock.createNiceMock(ServletContext.class);
+    final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+    final ByteArrayInputStream backendResponse = new ByteArrayInputStream("knox-backend".getBytes(StandardCharsets.UTF_8));
+
+    EasyMock.expect(inboundResponse.getStatusLine()).andReturn(statusLine).anyTimes();
+    EasyMock.expect(statusLine.getStatusCode()).andReturn(HttpStatus.SC_OK).anyTimes();
+    EasyMock.expect(inboundResponse.getEntity()).andReturn(entity).anyTimes();
+    EasyMock.expect(inboundResponse.getAllHeaders()).andReturn(new Header[0]).anyTimes();
+    EasyMock.expect(inboundRequest.getServletContext()).andReturn(context).anyTimes();
+    EasyMock.expect(entity.getContent()).andReturn(backendResponse).anyTimes();
+    EasyMock.expect(entity.getContentType()).andReturn(header).anyTimes();
+    EasyMock.expect(header.getElements()).andReturn(new HeaderElement[]{}).anyTimes();
+    EasyMock.expect(entity.getContentLength()).andReturn(4L).anyTimes();
+    EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(config).anyTimes();
+
+    HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class);
+    // Capture the status code when it is set on the response
+    Capture<Integer> statusCodeCapture = EasyMock.newCapture(CaptureType.FIRST);
+
+    outboundResponse.setStatus(EasyMock.captureInt(statusCodeCapture));
+
+    EasyMock.expectLastCall().once();
+    EasyMock.expect(outboundResponse.getOutputStream())
+        .andAnswer((IAnswer<SynchronousServletOutputStreamAdapter>) () -> new SynchronousServletOutputStreamAdapter() {
+          @Override
+          public void write( int b ) throws IOException {
+            throw new IOException( "unreachable-host" ); // Fail-over condition
+          }
+        }).once();
+
+    CloseableHttpClient mockHttpClient = EasyMock.createNiceMock(CloseableHttpClient.class);
+    EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes();
+
+    EasyMock.replay(filterConfig,
+        servletContext,
+        outboundRequest,
+        inboundRequest,
+        outboundResponse,
+        mockHttpClient,
+        inboundResponse,
+        statusLine,
+        entity,
+        header,
+        context,
+        config);
+
+    Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName));
+    ConfigurableHADispatch dispatch = new ConfigurableHADispatch();
+    dispatch.setHttpClient(mockHttpClient);
+    dispatch.setHaProvider(provider);
+    dispatch.setServiceRole(serviceName);
+    dispatch.init();
+
+    dispatch.executeRequestWrapper(outboundRequest, inboundRequest, outboundResponse);
+
+    Assert.assertEquals("Expected the request to have failed-over to the alternate host.", uri2, requestURICapture.getValue());
+    Assert.assertEquals("Expected the failed-over request to succeed.", HttpStatus.SC_OK, statusCodeCapture.getValue().intValue());
+
+  }
+
   private void doTestFailoverStickyOnFallbackOff(final Boolean withCookie)
       throws Exception {
     doTestFailoverStickyOnFallbackOff(withCookie, null);
diff --git a/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/AbstractHdfsHaDispatch.java b/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/AbstractHdfsHaDispatch.java
index 3e808fe..0c7865c 100644
--- a/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/AbstractHdfsHaDispatch.java
+++ b/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/AbstractHdfsHaDispatch.java
@@ -17,15 +17,13 @@
  */
 package org.apache.knox.gateway.hdfs.dispatch;
 
+import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.HttpRequestBase;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.entity.BufferedHttpEntity;
-import org.apache.knox.gateway.config.Configure;
 import org.apache.knox.gateway.filter.AbstractGatewayFilter;
-import org.apache.knox.gateway.ha.provider.HaProvider;
-import org.apache.knox.gateway.ha.provider.HaServiceConfig;
-import org.apache.knox.gateway.ha.provider.impl.HaServiceConfigConstants;
+import org.apache.knox.gateway.ha.dispatch.ConfigurableHADispatch;
 import org.apache.knox.gateway.hdfs.i18n.WebHdfsMessages;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 
@@ -38,13 +36,10 @@
 import java.nio.charset.StandardCharsets;
 import java.util.concurrent.atomic.AtomicInteger;
 
-public abstract class AbstractHdfsHaDispatch extends HdfsHttpClientDispatch {
+public abstract class AbstractHdfsHaDispatch extends ConfigurableHADispatch {
 
   private static final String FAILOVER_COUNTER_ATTRIBUTE = "dispatch.ha.failover.counter";
   private static final WebHdfsMessages LOG = MessagesFactory.get(WebHdfsMessages.class);
-  private int maxFailoverAttempts = HaServiceConfigConstants.DEFAULT_MAX_FAILOVER_ATTEMPTS;
-  private int failoverSleep = HaServiceConfigConstants.DEFAULT_FAILOVER_SLEEP;
-  private HaProvider haProvider;
 
   public AbstractHdfsHaDispatch() throws ServletException {
     super();
@@ -53,23 +48,10 @@
   @Override
   public void init() {
      super.init();
-     if (haProvider != null) {
-       HaServiceConfig serviceConfig = haProvider.getHaDescriptor().getServiceConfig(getResourceRole());
-       maxFailoverAttempts = serviceConfig.getMaxFailoverAttempts();
-       failoverSleep = serviceConfig.getFailoverSleep();
-     }
    }
 
-  public HaProvider getHaProvider() {
-    return haProvider;
-  }
-
   abstract String getResourceRole();
 
-  @Configure
-  public void setHaProvider(HaProvider haProvider) {
-    this.haProvider = haProvider;
-  }
 
   @Override
   protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException {
@@ -77,18 +59,21 @@
       try {
          inboundResponse = executeOutboundRequest(outboundRequest);
          writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse);
-      } catch (StandbyException e) {
-         LOG.errorReceivedFromStandbyNode(e);
-         failoverRequest(outboundRequest, inboundRequest, outboundResponse, inboundResponse, e);
-      } catch (SafeModeException e) {
-         LOG.errorReceivedFromSafeModeNode(e);
-         failoverRequest(outboundRequest, inboundRequest, outboundResponse, inboundResponse, e);
-      } catch (IOException e) {
-         LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
-         failoverRequest(outboundRequest, inboundRequest, outboundResponse, inboundResponse, e);
+      } catch (StandbyException | SafeModeException | IOException e) {
+        /* if non-idempotent requests are not allowed to failover */
+        if(!failoverNonIdempotentRequestEnabled && nonIdempotentRequests.stream().anyMatch(outboundRequest.getMethod()::equalsIgnoreCase)) {
+          LOG.cannotFailoverNonIdempotentRequest(outboundRequest.getMethod(), e.toString());
+          throw e;
+        } else {
+          printExceptionLogMessage(e, outboundRequest.getURI().toString());
+          failoverRequest(outboundRequest, inboundRequest, outboundResponse,
+              inboundResponse, e);
+        }
       }
    }
 
+
+
   /**
     * Checks for specific outbound response codes/content to trigger a retry or failover
     */
@@ -110,7 +95,8 @@
       super.writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, inboundResponse);
    }
 
-  private void failoverRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse, Exception exception) throws IOException {
+  @Override
+  protected void failoverRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, HttpResponse inboundResponse, Exception exception) throws IOException {
       LOG.failedToConnectTo(outboundRequest.getURI().toString());
       AtomicInteger counter = (AtomicInteger) inboundRequest.getAttribute(FAILOVER_COUNTER_ATTRIBUTE);
       if (counter == null) {
@@ -142,4 +128,33 @@
          }
       }
    }
+
+  /**
+   * This method ensures that the request InputStream is not acquired
+   * prior to a dispatch to a component such as a namenode that doesn't
+   * the request body. The side effect of this is that the client does
+   * not get a 100 continue from Knox which will trigger the client to
+   * send the entire payload before redirect to the target component
+   * like a datanode and have to send it again.
+   */
+  @Override
+  protected HttpEntity createRequestEntity(HttpServletRequest request)
+      throws IOException {
+    return null;
+  }
+
+  /**
+   * Helper method that prints descriptive log messages about the exception thrown.
+   * @param e Exception
+   * @param uri outbound uri
+   */
+  private void printExceptionLogMessage(final Exception e, String uri) {
+    if(e instanceof StandbyException) {
+      LOG.errorReceivedFromStandbyNode(e);
+    } else if(e instanceof SafeModeException) {
+      LOG.errorReceivedFromSafeModeNode(e);
+    } else {
+      LOG.errorConnectingToServer(uri, e);
+    }
+  }
 }
diff --git a/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/HdfsHttpClientDispatch.java b/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/HdfsHttpClientDispatch.java
index 9958d47..43adf3b 100644
--- a/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/HdfsHttpClientDispatch.java
+++ b/gateway-service-webhdfs/src/main/java/org/apache/knox/gateway/hdfs/dispatch/HdfsHttpClientDispatch.java
@@ -17,8 +17,8 @@
  */
 package org.apache.knox.gateway.hdfs.dispatch;
 
-import org.apache.knox.gateway.dispatch.ConfigurableDispatch;
 import org.apache.http.HttpEntity;
+import org.apache.knox.gateway.dispatch.ConfigurableDispatch;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
diff --git a/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java b/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java
index 0bd5e33..3e66d43 100644
--- a/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java
+++ b/gateway-service-webhdfs/src/test/java/org/apache/knox/gateway/hdfs/dispatch/WebHdfsHaDispatchTest.java
@@ -93,6 +93,7 @@
       CloseableHttpClient client = builder.build();
       dispatch.setHttpClient(client);
       dispatch.setHaProvider(provider);
+      dispatch.setServiceRole(serviceName);
       dispatch.init();
       long startTime = System.currentTimeMillis();
       try {
diff --git a/gateway-test/src/test/java/org/apache/knox/gateway/WebHdfsHaFuncTest.java b/gateway-test/src/test/java/org/apache/knox/gateway/WebHdfsHaFuncTest.java
index 388560e..401ad20 100644
--- a/gateway-test/src/test/java/org/apache/knox/gateway/WebHdfsHaFuncTest.java
+++ b/gateway-test/src/test/java/org/apache/knox/gateway/WebHdfsHaFuncTest.java
@@ -142,7 +142,7 @@
             .addTag("name").addText("HaProvider")
             .addTag("param")
             .addTag("name").addText("WEBHDFS")
-            .addTag("value").addText("maxFailoverAttempts=3;failoverSleep=15;enabled=true").gotoParent()
+            .addTag("value").addText("maxFailoverAttempts=5;failoverSleep=15;enabled=true;failoverNonIdempotentRequestEnabled=true").gotoParent()
             .gotoRoot()
             .addTag("service")
             .addTag("role").addText("WEBHDFS")