Use Netty to Proxy your requests

In this tutorial we will learn how to use Netty to set up a simple HTTP reverse proxy. We recommend checking this tutorial for an overview of Netty core concepts: Getting started with Netty

Setting up the Netty Project

Netty is a client / server framework which provides a simplified layer over NIO networking. This makes it a good candidate to create low level non blocking network application. There are several use cases for Netty: for example, you can use it to implement a TCP Proxy between a Client and Server.

Netty example distribution provides out of the box an example of HexDumpProxy. In our example, we will adapt it to work as an HTTP Proxy, by extending the SimpleChannelInboundHandler<String> Class.

Firstly, bootstrap a Maven project with your IDE or from the Command Line:

$ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4

Next, create the following HTTPProxy Class:

package io.netty.example;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public final class HTTPProxy {

    static final int LOCAL_PORT = Integer.parseInt(System.getProperty("localPort", "8081"));
    static final String REMOTE_HOST = System.getProperty("remoteHost", "localhost");
    static final int REMOTE_PORT = Integer.parseInt(System.getProperty("remotePort", "8080"));

    public static void main(String[] args) throws Exception {
        System.err.println("Proxying *:" + LOCAL_PORT + " to " + REMOTE_HOST + ':' + REMOTE_PORT + " ...");

        // Configure the bootstrap.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new HTTPProxyInitializer(REMOTE_HOST, REMOTE_PORT))
             .childOption(ChannelOption.AUTO_READ, false)
             .bind(LOCAL_PORT).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

As you can see, our example uses two EventLoopGroup. The first one for ServerBootstrap, the second is a Worker Group. This is a common best practice so that you can tune a specific Group for high load and detect bottlenecks.

Our HTTPProxy binds a LoggingHandler to dump the client/server communication and an HTTPProxyInitializer that wraps the Client/Server communication.

The HTTPProxyInitializer is a ChannelInitializer, that is, a special handler that you can use to configure a new Channel for your connection:

package io.netty.example;


import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public class HTTPProxyInitializer extends ChannelInitializer<SocketChannel> {

    private final String remoteHost;
    private final int remotePort;

    public HTTPProxyInitializer(String remoteHost, int remotePort) {
        this.remoteHost = remoteHost;
        this.remotePort = remotePort;
    }

    @Override
    public void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(
                new LoggingHandler(LogLevel.INFO),
                new HTTPProxyFrontendHandler(remoteHost, remotePort));
    }
}

Within the HTTPProxyInitializer, we have attached two Handlers to its ChannelPipeLine:

  1. A LoggingHandler in the first slot that logs all events using a logging framework.
  2. A Custom Handler named HTTPProxyFrontendHandler in the last slot.

The HTTPProxyFrontendHandler follows here:

package io.netty.example;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.SimpleChannelInboundHandler;

public class HTTPProxyFrontendHandler extends SimpleChannelInboundHandler<String> {  
	
    private final String remoteHost;
    private final int remotePort;
 
    private Channel outboundChannel;

    public HTTPProxyFrontendHandler(String remoteHost, int remotePort) {
        this.remoteHost = remoteHost;
        this.remotePort = remotePort;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        final Channel inboundChannel = ctx.channel();

        // Start the connection attempt.
        Bootstrap b = new Bootstrap();
        b.group(inboundChannel.eventLoop())
         .channel(ctx.channel().getClass())
         .handler(new HTTPProxyBackendHandler(inboundChannel))
         .option(ChannelOption.AUTO_READ, false);
        ChannelFuture f = b.connect(remoteHost, remotePort);
        outboundChannel = f.channel();
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                if (future.isSuccess()) {
                    // connection complete start to read first data
                    inboundChannel.read();
                } else {
                    // Close the connection if the connection attempt has failed.
                    inboundChannel.close();
                }
            }
        });
    }
 
    @Override
    public void channelRead0(final ChannelHandlerContext ctx, String msg) {
        if (outboundChannel.isActive()) {
            outboundChannel.writeAndFlush(msg).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    if (future.isSuccess()) {
                        // was able to flush out data, start to read the next chunk
                        ctx.channel().read();
                    } else {
                        future.channel().close();
                    }
                }
            });
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        if (outboundChannel != null) {
            closeOnFlush(outboundChannel);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        closeOnFlush(ctx.channel());
    }

 
    static void closeOnFlush(Channel ch) {
        if (ch.isActive()) {
            ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
        }
    }


}

The HTTPProxyFrontendHandler in turn registers a HTTPProxyBackendHandler to handle the Server response:

package io.netty.example;


import io.netty.channel.*;

import io.netty.channel.SimpleChannelInboundHandler;

public class HTTPProxyBackendHandler extends SimpleChannelInboundHandler<String> {  
    private final Channel inboundChannel;

    public HTTPProxyBackendHandler(Channel inboundChannel) {
        this.inboundChannel = inboundChannel;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.read();
    }

    @Override
    public void channelRead0(final ChannelHandlerContext ctx, String msg) {
        inboundChannel.writeAndFlush(msg).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                if (future.isSuccess()) {
                    ctx.channel().read();
                } else {
                    future.channel().close();
                }
            }
        });
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        HTTPProxyFrontendHandler.closeOnFlush(inboundChannel);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        HTTPProxyFrontendHandler.closeOnFlush(ctx.channel());
    }


}

Running the application

To build and run the application you need to include the following “umbrella” dependency in your project:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.52.Final</version>
</dependency>

Next, start your HTTPProxy from your IDE (“Run as Java application“) or the Command Line:

Proxying *:8081 to localhost:8080 ...

Then, start a server, for example WildFly, which binds on port 8080:

$ /home/jboss/wildfly-25.0.0.Final/bin/standalone.sh

Finally, point the browser to the Proxy address (localhost:8081)

curl http://localhost:8081

You will see from the Logs of your HTTPProxy, the traffic between the Client and the Server as seen from the Proxy:

INFO: [id: 0x800640f1, L:/127.0.0.1:8081 - R:/127.0.0.1:43894] READ: 471B
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..|
|00000010| 48 6f 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a |Host: localhost:|
|00000020| 38 30 38 31 0d 0a 55 73 65 72 2d 41 67 65 6e 74 |8081..User-Agent|
|00000030| 3a 20 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28 58 |: Mozilla/5.0 (X|
|00000040| 31 31 3b 20 4c 69 6e 75 78 20 78 38 36 5f 36 34 |11; Linux x86_64|
|00000050| 3b 20 72 76 3a 39 35 2e 30 29 20 47 65 63 6b 6f |; rv:95.0) Gecko|
|00000060| 2f 32 30 31 30 30 31 30 31 20 46 69 72 65 66 6f |/20100101 Firefo|
|00000070| 78 2f 39 35 2e 30 0d 0a 41 63 63 65 70 74 3a 20 |x/95.0..Accept: |
|00000080| 74 65 78 74 2f 68 74 6d 6c 2c 61 70 70 6c 69 63 |text/html,applic|
|00000090| 61 74 69 6f 6e 2f 78 68 74 6d 6c 2b 78 6d 6c 2c |ation/xhtml+xml,|
|000000a0| 61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 6d 6c 3b |application/xml;|
|000000b0| 71 3d 30 2e 39 2c 69 6d 61 67 65 2f 61 76 69 66 |q=0.9,image/avif|
|000000c0| 2c 69 6d 61 67 65 2f 77 65 62 70 2c 2a 2f 2a 3b |,image/webp,*/*;|
|000000d0| 71 3d 30 2e 38 0d 0a 41 63 63 65 70 74 2d 4c 61 |q=0.8..Accept-La|
|000000e0| 6e 67 75 61 67 65 3a 20 65 6e 2d 55 53 2c 65 6e |nguage: en-US,en|
|000000f0| 3b 71 3d 30 2e 35 0d 0a 41 63 63 65 70 74 2d 45 |;q=0.5..Accept-E|
|00000100| 6e 63 6f 64 69 6e 67 3a 20 67 7a 69 70 2c 20 64 |ncoding: gzip, d|
|00000110| 65 66 6c 61 74 65 0d 0a 43 6f 6e 6e 65 63 74 69 |eflate..Connecti|
|00000120| 6f 6e 3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d 0a |on: keep-alive..|
|00000130| 43 6f 6f 6b 69 65 3a 20 5f 67 61 3d 47 41 31 2e |Cookie: _ga=GA1.|
|00000140| 31 2e 31 30 38 39 33 37 35 35 38 39 2e 31 36 33 |1.1089375589.163|
|00000150| 37 35 30 35 31 37 35 0d 0a 55 70 67 72 61 64 65 |7505175..Upgrade|
|00000160| 2d 49 6e 73 65 63 75 72 65 2d 52 65 71 75 65 73 |-Insecure-Reques|
|00000170| 74 73 3a 20 31 0d 0a 53 65 63 2d 46 65 74 63 68 |ts: 1..Sec-Fetch|
|00000180| 2d 44 65 73 74 3a 20 64 6f 63 75 6d 65 6e 74 0d |-Dest: document.|
|00000190| 0a 53 65 63 2d 46 65 74 63 68 2d 4d 6f 64 65 3a |.Sec-Fetch-Mode:|
|000001a0| 20 6e 61 76 69 67 61 74 65 0d 0a 53 65 63 2d 46 | navigate..Sec-F|
|000001b0| 65 74 63 68 2d 53 69 74 65 3a 20 6e 6f 6e 65 0d |etch-Site: none.|
|000001c0| 0a 53 65 63 2d 46 65 74 63 68 2d 55 73 65 72 3a |.Sec-Fetch-User:|
|000001d0| 20 3f 31 0d 0a 0d 0a                            | ?1....         |
+--------+-------------------------------------------------+----------------+

Meanwhile, the browser will transparently return the Home page of the application server:

Conclusion

At the end of this Netty tutorial, you should be familiar with Netty network framework and how to create a simple Proxy Server. The source code (derived from the netty examples) is available here: https://github.com/fmarchioni/mastertheboss/tree/master/netty/proxy