Murp: the reverse proxy handler

A reverse proxy simply receives requests from a client and forwards them to another server. If you need to proxy requests to other serves, then you may consider using Murp: the Mu Reverse Proxy which is described below.

Step one: add the Murp dependency

<dependency>
    <groupId>io.muserver</groupId>
    <artifactId>mu-server</artifactId>
    <version>2.0.0</version>
</dependency>
<dependency>
    <groupId>io.muserver</groupId>
    <artifactId>murp</artifactId>
    <version>1.1.2</version>
</dependency>

Warning: Murp has a dependency on Jetty Client which brings in some Jetty dependencies. If your project already has direct or indirect dependencies on Jetty, check that the versions of the jetty-* libraries are all the same version.

Step two: create a Mu Server and add the reverse proxy as a handler

Murp does not start a server for you, instead you need to create your own server which allows you to configure it however you want (e.g. with HTTPS). The reverse proxy is just added like any other handler, which means you can add handlers before it (perhaps to add or remove custom headers) or after it (if the URI mapping function returns null then the next handler will be invoked).

The reverse proxy is configured with the ReverseProxyBuilder which has only one required configuration option: withUriMapper which takes a UriMapper. The URI mapper is how you convert an incoming request URI to the target URI.

In the following example, a target server is started on HTTP which simply replies with the requested URI and headers. The reverse proxy is started on HTTPS and just blindly forwards all requests to the target server.

public class ReverseProxyExample {
    public static void main(String[] args) {

        // Start a target server which requests will be proxied to
        MuServer targetServer = httpServer()
            .addHandler((req, resp) -> {
                resp.sendChunk("Received " + req + " with headers:\n\n");
                req.headers().forEach(e -> resp.sendChunk(e.getKey() + "=" + e.getValue() + "\n"));
                return true;
            })
            .start();
        System.out.println("The target server is at " + targetServer.uri());


        // Start a reverse proxy that sends all requests to the target server
        MuServer reverseProxy = httpsServer()
            .addHandler((req, resp) -> {
                req.headers().set("X-Send-To-Target", "A value sent to the target");
                resp.headers().set("X-Send-To-Client", "A value returned to the client");
                return false;
            })
            .addHandler(
                ReverseProxyBuilder.reverseProxy()
                    .withUriMapper(request -> {
                        // This maps the client's URI to the target URI, which in this demo
                        // is simply the target server's URI with the client's path and querystring
                        // appended.
                        String pathAndQuery = Murp.pathAndQuery(request.uri());
                        return targetServer.uri().resolve(pathAndQuery);
                    })
                    .sendLegacyForwardedHeaders(true) // Adds X-Forwarded-*
                    .withViaName("myreverseproxy")
                    .proxyHostHeader(false)
                    .addProxyCompleteListener((clientRequest, clientResponse, targetUri, durationMillis) -> {
                        System.out.println("Proxied " + clientRequest + " to " + targetUri +
                            " and returned " + clientResponse.status() + " in " + durationMillis + "ms");
                    })
            )
            .start();

        System.out.println("Reverse proxy started at " + reverseProxy.uri());
        System.out.println("Example URLs: " + reverseProxy.uri().resolve("/blah") + " / "
            + reverseProxy.uri().resolve("/blah?greeting=hello%20world"));
    }
}
(see full file)

Note that further customization of the proxied request and response can be done by adding listeners before the request is sent to the target with ReverseProxyBuilder.withRequestInterceptor(RequestInterceptor) and before the response is sent to the client with ReverseProxyBuilder.withResponseInterceptor(ResponseInterceptor).

The Host header

There are certain headers that reverse proxies do not forward, called Hop by Hop Headers. However, by default the Host header is supposed to be proxied from the client to the target server, which is the default in Murp.

However if proxying requests from the browser to another HTTPS website, you may get errors related to invalid Host headers, or related to SNI. This is because the Host header does not match the header that the target website matches.

In these cases, you can instruct Murp to not forward the Host header by calling .proxyHostHeader(false) on the ReverseProxyBuilder.

Timeouts

There are several types of time outs when working with reverse proxies:

All of these except the total timeout are defined on the HTTP client. The following example shows how to configure each of them:

public class ReverseProxyTimeoutsExample {
    public static void main(String[] args) {
        MuServer reverseProxy = httpServer()
            .addHandler(
                ReverseProxyBuilder.reverseProxy()
                    .withUriMapper(UriMapper.toDomain(URI.create("http://example.org")))
                    .proxyHostHeader(false)
                    .withTotalTimeout(20000) // 20 seconds
                    .withHttpClient(
                        ReverseProxyBuilder.createHttpClientBuilder(false)
                            .connectTimeout(Duration.ofSeconds(15))
                            .build()
                    )
            )
            .start();
        System.out.println("Reverse proxy started at " + reverseProxy.uri());
    }
}
(see full file)

If a timeout occurs before any headers are sent back to the client, then a 504 status code is sent to the client.