http-proxy: attach headers to connection exception (#8824)

Motivation:
When a proxy fails to connect, it includes useful error detail in
the headers.

Modification:
- Add an HTTP Specific ProxyConnectException
- Attach headers (if any) in the event of a non-200 response

Result:
Able to surface more useful error info to applications
This commit is contained in:
Carl Mastrangelo 2019-02-01 22:16:36 -08:00 committed by Norman Maurer
parent d96c02fc68
commit 2ae0fb19b6
2 changed files with 120 additions and 11 deletions

View File

@ -47,9 +47,10 @@ public final class HttpProxyHandler extends ProxyHandler {
private final String username; private final String username;
private final String password; private final String password;
private final CharSequence authorization; private final CharSequence authorization;
private final HttpHeaders outboundHeaders;
private final boolean ignoreDefaultPortsInConnectHostHeader; private final boolean ignoreDefaultPortsInConnectHostHeader;
private HttpResponseStatus status; private HttpResponseStatus status;
private HttpHeaders headers; private HttpHeaders inboundHeaders;
public HttpProxyHandler(SocketAddress proxyAddress) { public HttpProxyHandler(SocketAddress proxyAddress) {
this(proxyAddress, null); this(proxyAddress, null);
@ -66,7 +67,7 @@ public final class HttpProxyHandler extends ProxyHandler {
username = null; username = null;
password = null; password = null;
authorization = null; authorization = null;
this.headers = headers; this.outboundHeaders = headers;
this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader; this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
} }
@ -102,7 +103,7 @@ public final class HttpProxyHandler extends ProxyHandler {
authz.release(); authz.release();
authzBase64.release(); authzBase64.release();
this.headers = headers; this.outboundHeaders = headers;
this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader; this.ignoreDefaultPortsInConnectHostHeader = ignoreDefaultPortsInConnectHostHeader;
} }
@ -163,32 +164,59 @@ public final class HttpProxyHandler extends ProxyHandler {
req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization); req.headers().set(HttpHeaderNames.PROXY_AUTHORIZATION, authorization);
} }
if (headers != null) { if (outboundHeaders != null) {
req.headers().add(headers); req.headers().add(outboundHeaders);
} }
return req; return req;
} }
@Override @Override
protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception { protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws HttpProxyConnectException {
if (response instanceof HttpResponse) { if (response instanceof HttpResponse) {
if (status != null) { if (status != null) {
throw new ProxyConnectException(exceptionMessage("too many responses")); throw new HttpProxyConnectException(exceptionMessage("too many responses"), /*headers=*/ null);
} }
status = ((HttpResponse) response).status(); HttpResponse res = (HttpResponse) response;
status = res.status();
inboundHeaders = res.headers();
} }
boolean finished = response instanceof LastHttpContent; boolean finished = response instanceof LastHttpContent;
if (finished) { if (finished) {
if (status == null) { if (status == null) {
throw new ProxyConnectException(exceptionMessage("missing response")); throw new HttpProxyConnectException(exceptionMessage("missing response"), inboundHeaders);
} }
if (status.code() != 200) { if (status.code() != 200) {
throw new ProxyConnectException(exceptionMessage("status: " + status)); throw new HttpProxyConnectException(exceptionMessage("status: " + status), inboundHeaders);
} }
} }
return finished; return finished;
} }
/**
* Specific case of a connection failure, which may include headers from the proxy.
*/
public static final class HttpProxyConnectException extends ProxyConnectException {
private static final long serialVersionUID = -8824334609292146066L;
private final HttpHeaders headers;
/**
* @param message The failure message.
* @param headers Header associated with the connection failure. May be {@code null}.
*/
public HttpProxyConnectException(String message, HttpHeaders headers) {
super(message);
this.headers = headers;
}
/**
* Returns headers, if any. May be {@code null}.
*/
public HttpHeaders headers() {
return headers;
}
}
} }

View File

@ -15,20 +15,37 @@
*/ */
package io.netty.handler.proxy; package io.netty.handler.proxy;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.MultithreadEventLoopGroup;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalHandler;
import io.netty.channel.local.LocalServerChannel;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.proxy.HttpProxyHandler.HttpProxyConnectException;
import io.netty.util.NetUtil; import io.netty.util.NetUtil;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test; import org.junit.Test;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
public class HttpProxyHandlerTest { public class HttpProxyHandlerTest {
@ -153,6 +170,70 @@ public class HttpProxyHandlerTest {
true); true);
} }
@Test
public void testExceptionDuringConnect() throws Exception {
EventLoopGroup group = null;
Channel serverChannel = null;
Channel clientChannel = null;
try {
group = new MultithreadEventLoopGroup(1, LocalHandler.newFactory());
final LocalAddress addr = new LocalAddress("a");
final AtomicReference<Throwable> exception = new AtomicReference<Throwable>();
ChannelFuture sf =
new ServerBootstrap().channel(LocalServerChannel.class).group(group).childHandler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addFirst(new HttpResponseEncoder());
ch.pipeline().addFirst(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) {
DefaultFullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.BAD_GATEWAY);
response.headers().add("name", "value");
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0");
ctx.writeAndFlush(response);
}
});
}
}).bind(addr);
serverChannel = sf.sync().channel();
ChannelFuture cf = new Bootstrap().channel(LocalChannel.class).group(group).handler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addFirst(new HttpProxyHandler(addr));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
exception.set(cause);
}
});
}
}).connect(new InetSocketAddress("localhost", 1234));
clientChannel = cf.sync().channel();
clientChannel.close().sync();
assertTrue(exception.get() instanceof HttpProxyConnectException);
HttpProxyConnectException actual = (HttpProxyConnectException) exception.get();
assertNotNull(actual.headers());
assertEquals("value", actual.headers().get("name"));
} finally {
if (clientChannel != null) {
clientChannel.close();
}
if (serverChannel != null) {
serverChannel.close();
}
if (group != null) {
group.shutdownGracefully();
}
}
}
private static void testInitialMessage(InetSocketAddress socketAddress, private static void testInitialMessage(InetSocketAddress socketAddress,
String expectedUrl, String expectedUrl,
String expectedHostHeader, String expectedHostHeader,