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

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


The following commit(s) were added to refs/heads/master by this push:
     new b438768cd KNOX-3381: Fix HaDispatch unnecessary failover due to 
IOException during writeOutboundResponse (#1308)
b438768cd is described below

commit b438768cd87b96d0bcc677745e3c39acb5e4d72d
Author: hanicz <[email protected]>
AuthorDate: Mon Jul 20 08:09:24 2026 +0200

    KNOX-3381: Fix HaDispatch unnecessary failover due to IOException during 
writeOutboundResponse (#1308)
---
 .../gateway/ha/dispatch/AtlasApiHaDispatch.java    |  4 +-
 .../dispatch/AtlasApiTrustedProxyHaDispatch.java   |  4 +-
 .../knox/gateway/ha/dispatch/AtlasHaDispatch.java  |  4 +-
 .../ha/dispatch/AtlasTrustedProxyHaDispatch.java   |  4 +-
 .../ha/dispatch/ConfigurableHADispatch.java        |  3 +-
 .../ha/dispatch/ConfigurableHADispatchTest.java    | 86 ++++++++++++++++++++++
 .../gateway/ha/dispatch/DefaultHaDispatchTest.java | 26 ++++---
 .../knox/gateway/dispatch/NiFiHaDispatch.java      |  3 +-
 8 files changed, 113 insertions(+), 21 deletions(-)

diff --git 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiHaDispatch.java
 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiHaDispatch.java
index 59fbb9ec3..ce2b0ebb8 100644
--- 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiHaDispatch.java
+++ 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiHaDispatch.java
@@ -71,12 +71,12 @@ public class AtlasApiHaDispatch extends DefaultHaDispatch {
                 failoverRequest(outboundRequest, inboundRequest, 
outboundResponse, inboundResponse, new Exception("Atlas HA redirection"));
             }
 
-            writeOutboundResponse(outboundRequest, inboundRequest, 
outboundResponse, inboundResponse);
-
         } catch (IOException e) {
             LOG.errorConnectingToServer(outboundRequest.getURI().toString(), 
e);
             failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, e);
+            return;
         }
+        writeOutboundResponse(outboundRequest, inboundRequest, 
outboundResponse, inboundResponse);
     }
 
 }
diff --git 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiTrustedProxyHaDispatch.java
 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiTrustedProxyHaDispatch.java
index fdf4b91b4..e3948f1fc 100644
--- 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiTrustedProxyHaDispatch.java
+++ 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasApiTrustedProxyHaDispatch.java
@@ -44,11 +44,11 @@ public class AtlasApiTrustedProxyHaDispatch extends 
DefaultHaDispatch {
         failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, new Exception("Atlas HA redirection"));
       }
 
-      writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
-
     } catch (IOException e) {
       LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
       failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, e);
+      return;
     }
+    writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
   }
 }
diff --git 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasHaDispatch.java
 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasHaDispatch.java
index 788514927..4fb5ff610 100644
--- 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasHaDispatch.java
+++ 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasHaDispatch.java
@@ -64,12 +64,12 @@ public class AtlasHaDispatch extends DefaultHaDispatch {
                 }
             }
 
-            writeOutboundResponse(outboundRequest, inboundRequest, 
outboundResponse, inboundResponse);
-
         } catch (IOException e) {
             LOG.errorConnectingToServer(outboundRequest.getURI().toString(), 
e);
             failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, e);
+            return;
         }
+        writeOutboundResponse(outboundRequest, inboundRequest, 
outboundResponse, inboundResponse);
     }
 
     private boolean isLoginRedirect(Header locationHeader) {
diff --git 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasTrustedProxyHaDispatch.java
 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasTrustedProxyHaDispatch.java
index b42ae1e10..c9e7993ef 100644
--- 
a/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasTrustedProxyHaDispatch.java
+++ 
b/gateway-provider-ha/src/main/java/org/apache/knox/gateway/ha/dispatch/AtlasTrustedProxyHaDispatch.java
@@ -50,12 +50,12 @@ public class AtlasTrustedProxyHaDispatch extends 
ConfigurableHADispatch {
         }
       }
 
-      writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
-
     } catch (IOException e) {
       LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
       failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, e);
+      return;
     }
+    writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
   }
 
   private boolean isLoginRedirect(Header locationHeader) {
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 40382dd43..5c24f81a6 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
@@ -113,7 +113,6 @@ public class ConfigurableHADispatch extends 
ConfigurableDispatch implements Comm
     HttpResponse inboundResponse = null;
     try {
       inboundResponse = executeOutboundRequest(outboundRequest);
-      writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
     } catch ( IOException e ) {
       /* if non-idempotent requests are not allowed to failover, unless it's a 
connection error */
       if(!isConnectionError(e.getCause()) && 
isNonIdempotentAndNonIdempotentFailoverDisabled(outboundRequest)) {
@@ -125,7 +124,9 @@ public class ConfigurableHADispatch extends 
ConfigurableDispatch implements Comm
         LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
         failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, e);
       }
+      return;
     }
+    writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
   }
 
   protected void failoverRequest(HttpUriRequest outboundRequest, 
HttpServletRequest inboundRequest, HttpServletResponse outboundResponse, 
HttpResponse inboundResponse, Exception exception) throws IOException {
diff --git 
a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatchTest.java
 
b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatchTest.java
index accca102e..92f1942dc 100644
--- 
a/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatchTest.java
+++ 
b/gateway-provider-ha/src/test/java/org/apache/knox/gateway/ha/dispatch/ConfigurableHADispatchTest.java
@@ -198,4 +198,90 @@ public class ConfigurableHADispatchTest {
     Assert.assertEquals(DigestUtils.sha256Hex(activeURL), 
captureCookieValue.getValue().getValue());
   }
 
+  /**
+   * Test that a failure while copying an already-received backend response to 
the client
+   * (e.g. a parse/rewrite error thrown from writeOutboundResponse) does NOT 
trigger a failover.
+   * The backend has already served the request, so replaying it against 
another node would be wrong.
+   */
+  @Test
+  public void testNoFailoverWhenResponseCopyFails() throws Exception {
+    String serviceName = "OOZIE";
+    HaDescriptor descriptor = HaDescriptorFactory.createDescriptor();
+    
descriptor.addServiceConfig(HaDescriptorFactory.createServiceConfig(serviceName,
 "true", "1", "1000", null, null, null, null, null, null, null));
+    HaProvider provider = new DefaultHaProvider(descriptor);
+    URI uri1 = new URI( "http://host1.valid"; );
+    URI uri2 = new URI( "http://host2.valid"; );
+    ArrayList<String> urlList = new ArrayList<>();
+    urlList.add(uri1.toString());
+    urlList.add(uri2.toString());
+    provider.addHaService(serviceName, urlList);
+
+    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();
+
+    HttpServletRequest inboundRequest = 
EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(inboundRequest.getRequestURL()).andReturn( new 
StringBuffer(uri1.toString()) ).anyTimes();
+    
EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new
 AtomicInteger(0)).anyTimes();
+
+    /* 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);
+    EasyMock.expect(outboundResponse.getOutputStream()).andAnswer( new 
IAnswer<SynchronousServletOutputStreamAdapter>() {
+      @Override
+      public SynchronousServletOutputStreamAdapter answer() {
+        return new SynchronousServletOutputStreamAdapter() {
+          @Override
+          public void write( int b ) throws IOException {
+            throw new IOException( "response copy failed" );
+          }
+        };
+      }
+    }).once();
+
+    CloseableHttpClient mockHttpClient = 
EasyMock.createNiceMock(CloseableHttpClient.class);
+    
EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).once();
+
+    EasyMock.replay(outboundRequest, inboundRequest, outboundResponse, 
mockHttpClient, inboundResponse,
+            statusLine, entity, header, context, config);
+
+    ConfigurableHADispatch dispatch = new ConfigurableHADispatch();
+    dispatch.setHttpClient(mockHttpClient);
+    dispatch.setHaProvider(provider);
+    dispatch.setServiceRole(serviceName);
+    dispatch.init();
+
+    try {
+      dispatch.executeRequestWrapper(outboundRequest, inboundRequest, 
outboundResponse);
+      Assert.fail("Expected the response-copy IOException to propagate");
+    } catch (IOException e) {
+      Assert.assertEquals("response copy failed", e.getMessage());
+    }
+
+    EasyMock.verify(mockHttpClient);
+    Assert.assertEquals(uri1.toString(), provider.getActiveURL(serviceName));
+  }
+
 }
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 02c711ac5..288756ea5 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
@@ -828,17 +828,17 @@ public class DefaultHaDispatchTest {
 
     outboundResponse.setStatus(EasyMock.captureInt(statusCodeCapture));
 
-    EasyMock.expectLastCall().once();
+    EasyMock.expectLastCall().anyTimes();
     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
+            /* do nothing */
           }
-        }).atLeastOnce();
+        }).anyTimes();
 
     CloseableHttpClient mockHttpClient = 
EasyMock.createNiceMock(CloseableHttpClient.class);
-    
EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes();
+    EasyMock.expect(mockHttpClient.execute(outboundRequest)).andThrow(new 
IOException("unreachable-host")).andReturn(inboundResponse);
 
     EasyMock.replay(filterConfig,
         servletContext,
@@ -958,17 +958,17 @@ public class DefaultHaDispatchTest {
 
     outboundResponse.setStatus(EasyMock.captureInt(statusCodeCapture));
 
-    EasyMock.expectLastCall().once();
+    EasyMock.expectLastCall().anyTimes();
     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
+            /* do nothing */
           }
-        }).atLeastOnce();
+        }).anyTimes();
 
     CloseableHttpClient mockHttpClient = 
EasyMock.createNiceMock(CloseableHttpClient.class);
-    
EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes();
+    EasyMock.expect(mockHttpClient.execute(outboundRequest)).andThrow(new 
IOException("unreachable-host")).andReturn(inboundResponse);
 
     EasyMock.replay(filterConfig,
         servletContext,
@@ -1103,12 +1103,16 @@ public class DefaultHaDispatchTest {
             .andAnswer((IAnswer<SynchronousServletOutputStreamAdapter>) () -> 
new SynchronousServletOutputStreamAdapter() {
               @Override
               public void write( int b ) throws IOException {
-                throw new IOException( "unreachable-host" ); // Fail-over 
condition
+                /* do nothing */
               }
-            }).atLeastOnce();
+            }).anyTimes();
 
     CloseableHttpClient mockHttpClient = 
EasyMock.createNiceMock(CloseableHttpClient.class);
-    
EasyMock.expect(mockHttpClient.execute(outboundRequest)).andReturn(inboundResponse).anyTimes();
+    if (withCookie) {
+      EasyMock.expect(mockHttpClient.execute(outboundRequest)).andThrow(new 
IOException("unreachable-host")).once();
+    } else {
+      EasyMock.expect(mockHttpClient.execute(outboundRequest)).andThrow(new 
IOException("unreachable-host")).andReturn(inboundResponse);
+    }
 
     EasyMock.replay(filterConfig,
                     servletContext,
diff --git 
a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java
 
b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java
index 6d390a3db..04dbfe2e5 100644
--- 
a/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java
+++ 
b/gateway-service-nifi/src/main/java/org/apache/knox/gateway/dispatch/NiFiHaDispatch.java
@@ -42,11 +42,12 @@ public class NiFiHaDispatch extends DefaultHaDispatch {
     try {
       outboundRequest = NiFiRequestUtil.modifyOutboundRequest(outboundRequest, 
inboundRequest);
       inboundResponse = executeOutboundRequest(outboundRequest);
-      writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
     } catch (IOException e) {
       LOG.errorConnectingToServer(outboundRequest.getURI().toString(), e);
       failoverRequest(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse, e);
+      return;
     }
+    writeOutboundResponse(outboundRequest, inboundRequest, outboundResponse, 
inboundResponse);
   }
 
   /**

Reply via email to