Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<groupId>org.littleshoot</groupId>
<artifactId>littleproxy</artifactId>
<packaging>jar</packaging>
<version>1.1.3-VGS-SNAPSHOT</version>
<version>1.1.3.1-VGS-SNAPSHOT</version>
<name>LittleProxy</name>
<description>
LittleProxy is a high performance HTTP proxy written in Java and using the Netty networking framework.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.littleshoot.proxy;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in this class is copied from writeBadRequest method of ClientToProxyConnection class


import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.littleshoot.proxy.impl.ProxyUtils;

public class BadGatewayFailureHttpResponseComposer implements FailureHttpResponseComposer {

/**
* Tells the client that something went wrong trying to proxy its request. If the Bad Gateway is a response to
* an HTTP HEAD request, the response will contain no body, but the Content-Length header will be set to the
* value it would have been if this 502 Bad Gateway were in response to a GET.
*
* @param httpRequest the HttpRequest that is resulting in the Bad Gateway response
* @param cause raised exception
* @return true if the connection will be kept open, or false if it will be disconnected
*/
@Override
public FullHttpResponse compose(HttpRequest httpRequest, Throwable cause) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should unit test for this component.

String body = provideCustomMessage(httpRequest, cause);

FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);

if (ProxyUtils.isHEAD(httpRequest)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not tested.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

badgatewaytest
well, it is tested by the original littleproxy tests.. but okay, lets add additional test
@osklyarenko

// don't allow any body content in response to a HEAD request
response.content().clear();
}
return response;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the following part will be common between various implementations

    FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);

    if (ProxyUtils.isHEAD(httpRequest)) {
      // don't allow any body content in response to a HEAD request
      response.content().clear();
    }
    return response;

so we should abstract it away

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diff --git a/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java b/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java
index 26bcd78..b7e8abd 100644
--- a/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java
+++ b/src/main/java/org/littleshoot/proxy/BadGatewayFailureHttpResponseComposer.java
@@ -20,7 +20,7 @@ public final class BadGatewayFailureHttpResponseComposer implements ServerConnec
    */
   @Override
   public FullHttpResponse compose(HttpRequest httpRequest, Throwable cause) {
-    String body = "Bad Gateway: " + httpRequest.getUri();
+    String body = provideCustomMessage(httpRequest, cause);
 
     FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);
 
@@ -31,6 +31,10 @@ public final class BadGatewayFailureHttpResponseComposer implements ServerConnec
     return response;
   }
 
+  protected String provideCustomMessage(HttpRequest httpRequest, Throwable cause) {
+    return "Bad Gateway: " + httpRequest.getUri();
+  }
+
   public FullHttpResponse compose(HttpRequest httpRequest) {
     return this.compose(httpRequest, null);
   }

Copy link
Copy Markdown

@osklyarenko osklyarenko Jun 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this way you custom implementation would be a specialization of the default impl provided by BadGatewayFailureHttpResponseComposer.

}

/**
* The method can be overridden to provide a custom message along with 502 code
* @param httpRequest initial request
* @param cause an exception thrown on a failure
* @return custom message
*/
protected String provideCustomMessage(HttpRequest httpRequest, Throwable cause) {
return "Bad Gateway: " + httpRequest.getUri();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.littleshoot.proxy;

import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;

/**
* Interface for objects that can provide a custom http response on a specific failure.
*/
public interface FailureHttpResponseComposer {

/**
* Creates an {@link FullHttpResponse} based on initial request and failure cause
* @param httpRequest initial request
* @param cause an exception thrown during a failure
* @return failure http response
*/
FullHttpResponse compose(HttpRequest httpRequest, Throwable cause);
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ HttpProxyServerBootstrap withManInTheMiddle(
HttpProxyServerBootstrap withFiltersSource(
HttpFiltersSource filtersSource);

/**
* <p>
* Specify a {@link FailureHttpResponseComposer} to use for composing
* custom response message on unrecoverable failure
* </p>
*
* <p>
* Default = {@link BadGatewayFailureHttpResponseComposer}
* </p>
*
* @param unrecoverableFailureHttpResponseComposer custom response message composer
* @return
*/
HttpProxyServerBootstrap withUnrecoverableFailureHttpResponseComposer(
FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer);

/**
* <p>
* Specify whether or not to use secure DNS lookups for outbound
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import io.netty.util.concurrent.GenericFutureListener;
import org.apache.commons.lang3.StringUtils;
import org.littleshoot.proxy.ActivityTracker;
import org.littleshoot.proxy.BadGatewayFailureHttpResponseComposer;
import org.littleshoot.proxy.FailureHttpResponseComposer;
import org.littleshoot.proxy.FlowContext;
import org.littleshoot.proxy.FullFlowContext;
import org.littleshoot.proxy.HttpFilters;
Expand Down Expand Up @@ -616,22 +618,25 @@ protected boolean serverConnectionFailed(
serverConnection.getRemoteAddress(),
lastStateBeforeFailure,
cause);
connectionFailedUnrecoverably(initialRequest, serverConnection);
connectionFailedUnrecoverably(initialRequest, serverConnection, cause);
return false;
}
} catch (UnknownHostException uhe) {
connectionFailedUnrecoverably(initialRequest, serverConnection);
connectionFailedUnrecoverably(initialRequest, serverConnection, cause);
return false;
}
}

private void connectionFailedUnrecoverably(HttpRequest initialRequest, ProxyToServerConnection serverConnection) {
private void connectionFailedUnrecoverably(HttpRequest initialRequest, ProxyToServerConnection serverConnection, Throwable cause) {
// the connection to the server failed, so disconnect the server and remove the ProxyToServerConnection from the
// map of open server connections
serverConnection.disconnect();
this.serverConnectionsByHostAndPort.remove(serverConnection.getServerHostAndPort());

boolean keepAlive = writeBadGateway(initialRequest);
FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = proxyServer.getUnrecoverableFailureHttpResponseComposer();
FullHttpResponse failureResponse = unrecoverableFailureHttpResponseComposer.compose(initialRequest, cause);

boolean keepAlive = respondWithShortCircuitResponse(failureResponse);
if (keepAlive) {
become(AWAITING_INITIAL);
} else {
Expand Down Expand Up @@ -1203,15 +1208,8 @@ private void stripHopByHopHeaders(HttpHeaders headers) {
* @return true if the connection will be kept open, or false if it will be disconnected
*/
private boolean writeBadGateway(HttpRequest httpRequest) {
String body = "Bad Gateway: " + httpRequest.getUri();
FullHttpResponse response = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY, body);

if (ProxyUtils.isHEAD(httpRequest)) {
// don't allow any body content in response to a HEAD request
response.content().clear();
}

return respondWithShortCircuitResponse(response);
FullHttpResponse badGatewayResponse = new BadGatewayFailureHttpResponseComposer().compose(httpRequest, null);
return respondWithShortCircuitResponse(badGatewayResponse);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.netty.handler.traffic.GlobalTrafficShapingHandler;
import io.netty.util.concurrent.GlobalEventExecutor;
import org.littleshoot.proxy.ActivityTracker;
import org.littleshoot.proxy.BadGatewayFailureHttpResponseComposer;
import org.littleshoot.proxy.ChainedProxyManager;
import org.littleshoot.proxy.DefaultHostResolver;
import org.littleshoot.proxy.DnsSecServerResolver;
Expand All @@ -26,6 +27,7 @@
import org.littleshoot.proxy.HttpFiltersSourceAdapter;
import org.littleshoot.proxy.HttpProxyServer;
import org.littleshoot.proxy.HttpProxyServerBootstrap;
import org.littleshoot.proxy.FailureHttpResponseComposer;
import org.littleshoot.proxy.MitmManager;
import org.littleshoot.proxy.ProxyAuthenticator;
import org.littleshoot.proxy.SslEngineSource;
Expand Down Expand Up @@ -108,6 +110,7 @@ public class DefaultHttpProxyServer implements HttpProxyServer {
private final ChainedProxyManager chainProxyManager;
private final MitmManager mitmManager;
private final HttpFiltersSource filtersSource;
private final FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer;
private final boolean transparent;
private volatile int connectTimeout;
private volatile int idleConnectionTimeout;
Expand Down Expand Up @@ -240,6 +243,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
ChainedProxyManager chainProxyManager,
MitmManager mitmManager,
HttpFiltersSource filtersSource,
FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer,
boolean transparent,
int idleConnectionTimeout,
Collection<ActivityTracker> activityTrackers,
Expand All @@ -262,6 +266,7 @@ private DefaultHttpProxyServer(ServerGroup serverGroup,
this.chainProxyManager = chainProxyManager;
this.mitmManager = mitmManager;
this.filtersSource = filtersSource;
this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
this.transparent = transparent;
this.idleConnectionTimeout = idleConnectionTimeout;
if (activityTrackers != null) {
Expand Down Expand Up @@ -396,6 +401,7 @@ public HttpProxyServerBootstrap clone() {
chainProxyManager,
mitmManager,
filtersSource,
unrecoverableFailureHttpResponseComposer,
transparent,
idleConnectionTimeout,
activityTrackers,
Expand Down Expand Up @@ -581,6 +587,10 @@ public HttpFiltersSource getFiltersSource() {
return filtersSource;
}

public FailureHttpResponseComposer getUnrecoverableFailureHttpResponseComposer() {
return unrecoverableFailureHttpResponseComposer;
}

protected Collection<ActivityTracker> getActivityTrackers() {
return activityTrackers;
}
Expand Down Expand Up @@ -608,6 +618,7 @@ private static class DefaultHttpProxyServerBootstrap implements HttpProxyServerB
private ChainedProxyManager chainProxyManager = null;
private MitmManager mitmManager = null;
private HttpFiltersSource filtersSource = new HttpFiltersSourceAdapter();
private FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer = new BadGatewayFailureHttpResponseComposer();
private boolean transparent = false;
private int idleConnectionTimeout = 70;
private Collection<ActivityTracker> activityTrackers = new ConcurrentLinkedQueue<ActivityTracker>();
Expand Down Expand Up @@ -638,6 +649,7 @@ private DefaultHttpProxyServerBootstrap(
ChainedProxyManager chainProxyManager,
MitmManager mitmManager,
HttpFiltersSource filtersSource,
FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer,
boolean transparent, int idleConnectionTimeout,
Collection<ActivityTracker> activityTrackers,
int connectTimeout, HostResolver serverResolver,
Expand All @@ -659,6 +671,7 @@ private DefaultHttpProxyServerBootstrap(
this.chainProxyManager = chainProxyManager;
this.mitmManager = mitmManager;
this.filtersSource = filtersSource;
this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
this.transparent = transparent;
this.idleConnectionTimeout = idleConnectionTimeout;
if (activityTrackers != null) {
Expand Down Expand Up @@ -797,6 +810,12 @@ public HttpProxyServerBootstrap withFiltersSource(
return this;
}

public HttpProxyServerBootstrap withUnrecoverableFailureHttpResponseComposer(
FailureHttpResponseComposer unrecoverableFailureHttpResponseComposer) {
this.unrecoverableFailureHttpResponseComposer = unrecoverableFailureHttpResponseComposer;
return this;
}

@Override
public HttpProxyServerBootstrap withUseDnsSec(boolean useDnsSec) {
if (useDnsSec) {
Expand Down Expand Up @@ -900,7 +919,7 @@ private DefaultHttpProxyServer build() {
transportProtocol, determineListenAddress(),
sslEngineSource, authenticateSslClients,
proxyAuthenticator, chainProxyManager, mitmManager,
filtersSource, transparent,
filtersSource, unrecoverableFailureHttpResponseComposer, transparent,
idleConnectionTimeout, activityTrackers, connectTimeout,
serverResolver, readThrottleBytesPerSecond, writeThrottleBytesPerSecond,
localAddress, proxyAlias, maxInitialLineLength, maxHeaderSize, maxChunkSize,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.littleshoot.proxy;

import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import org.junit.Test;

import java.io.IOException;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class BadGatewayFailureHttpResponseComposerTest {

private static final String REQUEST_URI = "https://localhost/hi";

@Test
public void testDefault() throws IOException {
FailureHttpResponseComposer badGatewayResponseComposer = new BadGatewayFailureHttpResponseComposer();

HttpRequest initialRequest = mock(HttpRequest.class);
when(initialRequest.getUri()).thenReturn(REQUEST_URI);

FullHttpResponse response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());

assertEquals(502, response.getStatus().code());
assertEquals("Bad Gateway", response.getStatus().reasonPhrase());
assertEquals("Bad Gateway: " + REQUEST_URI, new String(response.content().array()));
}

@Test
public void testCustomMessage() throws IOException {
FailureHttpResponseComposer badGatewayResponseComposer = new BadGatewayFailureHttpResponseComposer() {
@Override
protected String provideCustomMessage(HttpRequest httpRequest, Throwable cause) {
return "Invalid certificate: " + httpRequest.getUri();
}
};

HttpRequest initialRequest = mock(HttpRequest.class);
when(initialRequest.getUri()).thenReturn(REQUEST_URI);

FullHttpResponse response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());

assertEquals(502, response.getStatus().code());
assertEquals("Bad Gateway", response.getStatus().reasonPhrase());
assertEquals("Invalid certificate: " + REQUEST_URI, new String(response.content().array()));
}

@Test
public void testClearedContent() throws IOException {
FailureHttpResponseComposer badGatewayResponseComposer = new BadGatewayFailureHttpResponseComposer();

HttpRequest initialRequest = mock(HttpRequest.class);
when(initialRequest.getUri()).thenReturn(REQUEST_URI);

FullHttpResponse response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());

assertEquals(502, response.getStatus().code());

assertEquals(0, response.content().readerIndex());
assertNotEquals(0, response.content().writerIndex());

when(initialRequest.getMethod()).thenReturn(HttpMethod.HEAD);

response = badGatewayResponseComposer.compose(initialRequest, new RuntimeException());

assertEquals(502, response.getStatus().code());

assertEquals(0, response.content().readerIndex());
assertEquals(0, response.content().writerIndex());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.littleshoot.proxy.impl;

import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import org.junit.Test;
import org.littleshoot.proxy.BadGatewayFailureHttpResponseComposer;
import org.littleshoot.proxy.FailureHttpResponseComposer;

import static org.junit.Assert.assertTrue;

public class DefaultHttpProxyServerTest {

@Test
public void testDefaultUnrecoverableFailureHttpResponseComposer() {
DefaultHttpProxyServer httpProxyServer = (DefaultHttpProxyServer) DefaultHttpProxyServer.bootstrap().start();
assertTrue(httpProxyServer.getUnrecoverableFailureHttpResponseComposer() instanceof BadGatewayFailureHttpResponseComposer);
httpProxyServer.stop();
}

@Test
public void testCustomUnrecoverableFailureHttpResponseComposer() {

class CustomUnrecoverableFailureHttpResponseComposer implements FailureHttpResponseComposer {
@Override
public FullHttpResponse compose(HttpRequest httpRequest, Throwable cause) {
return null;
}
}

DefaultHttpProxyServer httpProxyServer = (DefaultHttpProxyServer) DefaultHttpProxyServer
.bootstrap()
.withUnrecoverableFailureHttpResponseComposer(new CustomUnrecoverableFailureHttpResponseComposer())
.start();
assertTrue(httpProxyServer.getUnrecoverableFailureHttpResponseComposer() instanceof CustomUnrecoverableFailureHttpResponseComposer);
httpProxyServer.stop();
}

}