Lab Notes

Things I want to remember how to do.

JAX-RS Microservices

August 19, 2017

An example project where we have taken a Java library (java-xirr) and expose it as a REST service using JAX-RS. Then the service is dockerized using the maven dockerfile plugin. On the client side, we create a separate client using the Node.js connect server in order to illustrate various issues with CORS when utilizing REST services.

The project is available on GitHub as rest-xirr.

The REST service

Configuration

Turning a library into a REST service using JAX-RS in JEE 7 is pretty easy using the @ApplicationPath, @Path, @GET and @POST annotations, This has been covered extensively elsewhere so I won’t go into details here.

We want our REST service to use JSON to interact with the client, so we add @Consumes and @Produces annotations to our XirrService.xirr() method (full source):

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public XirrResult xirr(TxRecord[] records) {
        try {
            // Convert TxRecords into Transactions
            final List<Transaction> tx = Stream.of(records)
                .map(TxRecord::toTransaction)
                .collect(Collectors.toList());
            final double xirr = new Xirr(tx).xirr();
            // Wrap result in result object
            return new XirrResult(xirr);
        } catch (IllegalArgumentException iae) {
            // Convert IAEs thrown by Xirr into ServiceExceptions for the
            // exception mapper
            throw new ServiceException(iae);
        }
    }

The JAX-RS implementation will handle consuming the JSON and converting it to an array of TxRecord instances. It will also convert the return value, XirrResult, into JSON for the client.

JSON Details

Let’s drill into the rest of the xirr() method. First, you may notice that we are using a new class, TxRecord for the input instead of the Transaction class provided by the xirr library. The reason for this is that the JSON deserializer provided by JAX-RS will not work with immutable classes. So TxRecord is a mutable wrapper around Transaction.

For the return value, we could have just returned a double, and that would have been fine, but instead we created a wrapper object to see the JSON serialization in action.

Error Handling

Finally you might notice we are catching the IllegalArgumentExceptions throws by the xirr library and converting them into a custom exception, ServiceException. This allows us to define an ExceptionMapper to handle the errors (full source):

@Provider
@Singleton
public class ServiceExceptionMapper implements ExceptionMapper<ServiceException> {

    @Override
    public Response toResponse(ServiceException exception) {
        // Send the exception details to the client
        // A production version would probably use a custom error object
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
            .entity(exception)
            .build();
    }

}

The result is that when an IllegalArgumentException is thrown by the xirr library, it is converted in a ServiceException by the XirrService and then the above ExceptionMapper implementation kicks in. So instead of the default HTML page served by WildFly when an exception is thrown, the client gets the JSON encoding of the exception.

Originally I had the ExceptionMapper implementation extending ExceptionMapper<Exception>, but that ended up being too broad. When the client was sending the CORS preflight check (OPTIONS HTTP method), WildFly was throwing an exception in the process of generating the default OPTIONS reply required by the JAX-RS specfication. Then there was an additional issue with the mapping and the end result was the preflight check always failed and the following stacktrace emitted:

13:50:06,423 ERROR [io.undertow.request] (default task-52) UT005023: Exception handling request to /xirr: org.jboss.resteasy.spi.UnhandledException: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response object of type: org.jboss.resteasy.spi.DefaultOptionsMethodException of media type: application/octet-stream
	at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:187)
	at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:206)
	at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:221)
	at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:56)
	at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.service(HttpServletDispatcher.java:51)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
// trimmed here for brevity
Caused by: org.jboss.resteasy.core.NoMessageBodyWriterFoundFailure: Could not find MessageBodyWriter for response object of type: org.jboss.resteasy.spi.DefaultOptionsMethodException of media type: application/octet-stream
	at org.jboss.resteasy.core.ServerResponseWriter.writeNomapResponse(ServerResponseWriter.java:66)
	at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:183)
	... 42 more

Dockerization

Finally what microservice would be complete without a Docker image? The Dockerfile is simple and uses WildFly as the application server, but there is no reason any other compliant server could not be used (full source):

FROM jboss/wildfly:latest
ADD target/*.war /opt/jboss/wildfly/standalone/deployments/

The dockerfile maven plugin from Spotify has been integrated in order to convert the Dockerfile into an image. If you want to build and run a Docker container for the service:

$ mvn clean install dockerfile:build
$ docker run -p 8080:8080 --name xirr-rest decampo/xirr-rest:1.0.0-SNAPSHOT

Note that in order for the dockerfile plugin to work you must have Docker listening on port 2375 over TCP. If this is an issue of course the Docker image may still be built with the docker command as long as you have built the xirr.war first using Maven.

Configuring Docker on Fedora

This should probably be a separate post, but very quickly, let me describe how to configure Docker on Fedora to listen on port 2375. This should not be done lightly however as it is a local system privlege escalation risk.

First, create or edit the /etc/docker/daemon.json file to contain:

{
    "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"]
}

Then in bash:

# Create a docker group
$ sudo groupadd docker
# Add yourself to the group
$ sudo gpasswd -a ${USER} docker
# Restart the docker service
$ sudo systemctl restart docker
# Refresh your groups (or log out and log back in for GUI applications)
$ newgrp docker

Testing with curl

Once the container is up and running we can test it with curl:

# Test the ping service:
$ curl http://localhost:8080/xirr

# Test the xirr service:
$ curl -H "Content-type: application/json" -X POST \
       -d '[ { "amount":-1000, "when":"2017-01-01" }, 
             { "amount": 1100, "when":"2018-01-01" } ]' \
       http://localhost:8080/xirr/

# Test the xirr service and force an error condition:
$ curl -H "Content-type: application/json" -X POST \
       -d '[ { "amount": 1000, "when":"2017-01-01" }, 
             { "amount": 1100, "when":"2018-01-01" } ]' \
       http://localhost:8080/xirr/

REST Client

We could have built the client inside the war for the service, but that’s not a realistic scenario for actual deployment. So instead we build a client that is served from a separate server and tries to contact the xirr REST service directly. That puts us directly into the crosshairs of Cross-Origin Resource Sharing, aka CORS.

CORS HTTP Headers

Normally a web browser will not allow JavaScript code to connect to a server that did not serve that code for security reasons. CORS is a scheme contocted by the browser makers to allow a server to indicate that it can accept connections from other domains. Keep in mind that CORS is enforced on the client and thus you can’t really rely on it to supply security for the server.

To allow arbitrary clients to connect to our xirr REST service, we need for it to satisfy a couple of requirements. First, it should respond properly to requests made via the HTTP OPTIONS method. As noted above, the JAX-RS specification requires the implementation to generate a proper response for OPTIONS requests if the application does not specify a different one. So we are fine in that respect.

Second, the service needs to set a number of HTTP headers to specify the required security settings to the client. For this we define a JAX-RS response filter which will be applied to every response from our service (full source):

@Provider
public class CorsHeadersFilter implements ContainerResponseFilter {

    @Override
    public void filter(
        final ContainerRequestContext requestContext,
        final ContainerResponseContext responseContext) throws IOException {
        final MultivaluedMap<String, Object> headers =
            responseContext.getHeaders();
        // Following allows for all clients
        headers.add("Access-Control-Allow-Origin", "*");
        headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS);
        headers.add("Access-Control-Allow-Credentials", "true");
        headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
        // Set the length of time in seconds the preflight check may be cached
        // Use 1 second here for development purposes
        headers.add("Access-Control-Max-Age", "1");
    }
}

The @Provider annotation indicates to JAX-RS that this should be used whenever a ContainerResponseFilter is needed.

In this case ALLOWED_HEADERS is "Authorization, Content-Type". You may include any header you want, including custom headers. If a header is included in the request that is not whitelisted, the request is disallowed (by the browser). A list of automatically whitelisted headers is available at https://fetch.spec.whatwg.org/#cors-safelisted-request-header.

For ALLOWED_METHODS, I’ve used "DELETE, GET, HEAD, OPTIONS, POST, PUT".

We can test out the CORS configuration using curl:

# Test preflight request
$ curl -i -X OPTIONS http://localhost:8080/xirr

# Test headers on normal request:
$ curl -D - http://localhost:8080/xirr

Simple Node.js web server

For hosting the REST client, we use a simple Node.js web server, connect and run it using grunt via the grunt-contrib-connect plugin (full source of Gruntfile.js):

module.exports = function(grunt) {
    'use strict';
    
    grunt.loadNpmTasks('grunt-contrib-connect');
    
    grunt.initConfig({});

    // Simple HTTP server to host the xirr client
    // Runs on port 8000 by default
    grunt.config('connect', {
        options: {
            base: 'www',
            keepalive: true
        },
        'default': {}
    });
};

Then the client server (ugh, server for the client?) can be started with the command grunt connect. By using the keepalive option the server will continue to run.

JavaScript REST Client

For the JavaScript REST Client an ES6 class is created, XirrClient. A class method is created corresponding to the two service methods on the XirrService Java class, ping() and xirr().

The ping() method uses fetch() (see Using Fetch from MDN for more details) in it’s simplest form (full source):

    ping() {
        return fetch('http://localhost:8080/xirr').then(function(response) {
            console.log(response);
            return response.text();
        });
    }

The fetch() method returns a Promise object with a Response payload. The ping() method converts that into a Promise with a string payload for the caller.

The xirr() method uses fetch() in a more complicated way, since we need to deliver a JSON payload via POST for the xirr REST endpoint (full source):

    xirr() {
        return fetch('http://localhost:8080/xirr', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'}, 
            body: JSON.stringify(this.txs)
        }).then(function(response) {
            console.log(response);
            return response.json();
        });
    }

Note that using this form of fetch() allows us to send custom headers, including, if desired, an Authentication header for Open ID.

Another interesting aspect of this is the Promise returned by fetch() does not reject based on the HTTP status code of the response. In other words, when status code 500 is returned by our xirr REST service because of invalid input, we still end up in the normal success handler.

In this case we convert the Promise payload into the parsed JSON object returned by the server. So the xirr() method will return an object with a xirr property on success and a message property (from the serialized exception) when the xirr throws an IllegalArgumentException.

You can see the client in action in the index.js file (full source):

    client.clear()
      .add(-1000, "2017-01-01")
      .add( 1100, "2018-01-01")
      .xirr().then(function(result) {
        console.log(result);
        document.getElementById('output').innerHTML = 
            result.xirr ? formatter.format(result.xirr) : result.message;
    }).catch(errorHandler);

Summary

So, we have covered quite a bit here. Probably should have been a couple of blog posts, but in the end everything is so cross-referenced I felt it works better as a whole.

We wrapped a Java library with REST using JAX-RS and then dockerized the result. The REST service was configured to allow for CORS requests. Then we created a client from browser-based JavaScript land which consumes the service.

Resources