I do a lot of work with both ratpack and dropwizard for SmartThings. Dropwizard has a lot more opinions than Ratpack does out of the box, but they’re opinions I’ve grown used to and that I mostly agree with. I wanted validation in Ratpack to work as similarly as it does in Dropwizard.

Requirements

  • Use JSR-303
  • Be (mostly) automatic. I didn’t want to add much code to each handler to validate and send the error response
  • Not use exceptions for flow control.

Solution

The solution is pretty simple and it uses the Promise.route method to short circuit the promise chain and call clientError(int statusCode) immediately. Here’s a complete example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.apache.http.HttpStatus
import org.hibernate.validator.constraints.NotEmpty
import ratpack.exec.Promise
import ratpack.handling.Context
import ratpack.jackson.Jackson

import javax.validation.Validation
import javax.validation.Validator
import javax.validation.constraints.NotNull

import static ratpack.groovy.Groovy.ratpack

class Widget {
  @NotNull
  @NotEmpty
  String name
}

ratpack {
  bindings {
      bindInstance(Validator, Validation.buildDefaultValidatorFactory().validator)
  }

  handlers {
      post("widget") {
          parseAndValidate(context, Widget)
                  .map(Jackson.&json)
                  .then(context.&render)
      }
  }
}

private <T> Promise<T> parseAndValidate(Context ctx, Class<T> type) {
  return ctx.parse(Jackson.fromJson(type)).route({ T obj ->
      return (ctx.get(Validator).validate(obj).size() > 0)
  }, {
      ctx.clientError(HttpStatus.SC_UNPROCESSABLE_ENTITY)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
plugins {
    id 'io.ratpack.ratpack-groovy' version '1.3.3'
    id 'com.github.johnrengelman.shadow' version '1.2.3'
}

repositories {
    jcenter()
}

dependencies {
    compile 'org.hibernate:hibernate-validator:5.2.2.Final'
}

The parseAndValidate function parses the json just like you would normally o, then it calls the route method on promise and validates the the POGO using the Hibernate Validator. If there are any validation errors, route will short-circuit the promise chain and call clientError with a 422 status code.

This is solution is similar to the Mr. Haki article Ratpacked: Validating Forms: Validating Forms

It has been a while since I wrote another entry in this series but I was energized at Greach 2016 and I wanted to finish it.

The next step in the reverse proxy is to add additional features. Each new feature is implemented as an additional handler and added to the chain before the handler which will actually call the proxy target and stream the response.

There’s a lot of code in this post. You can see the entire project on github. I’m not including the tests for the new handlers in this post because they are very similar to the functional test in part 2 and you can see them on github.

Refactoring to Modules

The first step is to refactor the existing code into modules. Modules are the top level organizational unit for functionality in Ratpack. In this application there are two modules, an Administration module and a Proxy module.

1
2
3
4
5
6
7
8
9
10
package reverseproxy.admin

import com.google.inject.AbstractModule

class AdminModule extends AbstractModule {
  @Override
  protected void configure() {
      bind(ConfigHandler)
  }
}

The administration module doesn’t have much to it. The ConfigHandler is bound to the guice registry and that’s it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ProxyModule extends AbstractModule {
  @Override
  protected void configure() {

  }

  static class Config {
      String forwardToHost = 'InvalidHost'
      Integer forwardToPort = 80
      String forwardToScheme = 'http'
      Boolean logRequests = false
      List<Pattern> filterOut = []

      String canaryHost = null
      Integer canaryPort = null
      String canaryScheme = null
      Integer canaryPercentage = null

      boolean isCanaryEnabled() {
          canaryHost && canaryPort && canaryScheme && canaryPercentage
      }
  }

  static ProxyHandler proxyHandler() {
      return new ProxyHandler()
  }

  static LoggingHandler loggingHandler() {
      return new LoggingHandler()
  }

  static CanaryRoutingHandler canaryHandler() {
      return new CanaryRoutingHandler()
  }

  static BlacklistHandler blacklistHandler() {
      return new BlacklistHandler()
  }
}

The proxy module is similar to the Admin module, but with new handlers for each feature that is being added to the reverse proxy. The config object also became an internal class of the module which is idiomatic Ratpack. The static methods to retrieve handler instances is also idiomatic Ratpack, when you don’t need (or want) to use guice. You’ll notice it in many of the default Ratpack modules. The ratpack.groovy script needs updated to reflect the new modules.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ratpack {
  serverConfig { d -> d
    .props(Resources.asByteSource(Resources.getResource('application.properties')))
    .env()
    .sysProps()
    .require('/proxyConfig', ProxyModule.Config)
  }

  bindings {
    module HandlebarsModule
    module ProxyModule
    module AdminModule
  }

  handlers {
    get('reverseProxyAdmin', ConfigHandler)

    all ProxyModule.loggingHandler()
    all ProxyModule.blacklistHandler()
    all ProxyModule.canaryHandler()
    all ProxyModule.proxyHandler()
  }
}

Each of the new handlers is added to the chain.

The handlers

The logging handler. This handler simply logs the request and then executes the next method to move down the chain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
class LoggingHandler extends GroovyHandler {
  @Override
  protected void handle(GroovyContext context) {
      context.with {
          ProxyModule.Config config = context.get(ProxyModule.Config)

          if (config.logRequests) {
              Request request = context.request
              request.body.then { TypedData body ->
                  Blocking.exec {
                      String logMessage  = """path=${request.path}
                                            method=${request.method.name}
                                            params=${request.queryParams}
                                             body=${body.text}
                                         """.stripIndent()
                      log.info(logMessage)
                  }
              }

          }
          next()
      }
  }
}

The blacklist handler. This handler matches all the request uris against a regular expression and does not forward the request on to the proxy target.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BlacklistHandler extends GroovyHandler {
  @Override
  protected void handle(GroovyContext context) {
      context.with {
          ProxyModule.Config config = context.get(ProxyModule.Config)

          if (config.filterOut.any { it.matcher(context.request.path).matches() }) {
              context.render("Request path has been blacklisted")
          } else {
              next()
          }
      }
  }
}

The configuration is retrieved from the registry and then the request path matches any of the configured blacked out urls. If so, the handler calls context.render and does not call next. That means the rest of the handlers in the chain won’t be executed. Generally, handlers which render a response will not call next.

The last and most interesting new handler is the canary handler. This handler allows a percentage of requests to be proxied to a canary target instead of the normal target.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class CanaryRoutingHandler extends GroovyHandler {
  @Override
  protected void handle(GroovyContext context) {
      context.with {
          ProxyModule.Config config = context.get(ProxyModule.Config)
          URI requestURI = new URI(request.rawUri)
          URI proxyUri

          if (config.canaryEnabled) {
              Random random = ThreadLocalRandom.current()
              Long randomLong = random.nextLong()

              if (randomLong % 100 <= config.canaryPercentage) {
                  proxyUri = new URI(
                          config.canaryScheme,
                          requestURI.userInfo,
                          config.canaryHost,
                          config.canaryPort,
                          requestURI.path,
                          requestURI.query,
                          requestURI.fragment)
              }
          }

          if (!proxyUri) {
              proxyUri = new URI(
                      config.forwardToScheme,
                      requestURI.userInfo,
                      config.forwardToHost,
                      config.forwardToPort,
                      requestURI.path,
                      requestURI.query,
                      requestURI.fragment)

          }

          next(Registry.single(proxyUri))
      }
  }
}

The handler builds a URI instance containing either the canary URI or the normal URI and then calls next but in this case, we pass the URI instance inside the registry to the last and final handler. New items can be added to a registry which allows a downstream handler to change it’s behavior.

In order the use the URI added to the registry, the ProxyHandler had to be changed slightly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProxyHandler extends GroovyHandler {
  @Override
  protected void handle(GroovyContext context) {
      context.with {
          HttpClient httpClient = context.get(HttpClient)
          URI proxyUri = context.get(URI)

          httpClient.requestStream(proxyUri) { RequestSpec spec ->
              spec.headers.copy(request.headers)
          }.then { StreamedResponse responseStream ->
              responseStream.forwardTo(response)
          }
      }
  }
}

The only change here is that the handler retrieves an instance of URI from the registry rather than building one from the configuration instance.

Handler chain execution and ordering

The handler chain is probably the most critical piece of Ratpack to understand. Walking through what happens when a request is processed is a useful mental exercise. The chain is constructed in the ratpack.groovy script.

1
2
3
4
5
6
7
8
handlers {
    get('reverseProxyAdmin', AdminModule.configHandler())

    all ProxyModule.loggingHandler()
    all ProxyModule.blacklistHandler()
    all ProxyModule.canaryHandler()
    all ProxyModule.proxyHandler()
  }

Consider a request for /reverseProxyAdmin. The first handler processed is the one created by get('reverseProxyAdmin', AdminModule.configHandler()). In this case, the request path matches what was passed to the get method, and so the ConfigHandler.handle() method is executed. This method executes render handlebarsTemplate('reverseProxyAdmin.html', config: config) and never calls context.next(). The handler chain stops executing the html rendered in that method is written as the response body text.

Now consider a request for /randomUri. The handler chain starts to execute and randomUri does not match /reverseProxyAdmin, therefor ConfigHandler.handle() is not executed. The next handler in the chain was added by all ProxyModule.loggingHandler(). So LoggingHandler.handle() is executed. That method always calls context.next(). The next handler in the chain is the blacklist handler. That handler was also added for every request (via the all method) and so that handler executes. The handler chain continues to execute in this manner until a handler does not call context.next().

What would happen if the handler chain was defined slightly differently?

1
2
3
4
5
6
7
8
9
handlers {
  all ProxyModule.loggingHandler()

    get('reverseProxyAdmin', AdminModule.configHandler())

    all ProxyModule.blacklistHandler()
    all ProxyModule.canaryHandler()
    all ProxyModule.proxyHandler()
  }

In the case of /reverseProxyAdmin, the first handler processed is the one created by all ProxyModule.loggingHandler(). The handler logs the request and then calls context.next(). The next handler is now the ConfigHandler, and because the path matches reverseProxyAdmin it is executed. Everything else is the same. Order is important when constructing the handler chain. Changing the handler order will change behavior.

Next time.

In the next post I’ll talk about creating a service layer to delegate functionality to from handlers, unit testing handlers and persistence using JOOQ.

Over the last few weeks I have upgraded my pet project CellarHQRatpack from the ancient Ratpack 0.9.15 to the current 1.1.1, and also moved it from being hosted on AWS Elastic Beanstalk to Heroku. In the process I redid the configuration more than 1 time. The documentation while accurate, lacks any concrete examples.

Loading configuration in Ratpack

Ratpack can be configured in several different ways, including properties files, yaml, system properties and environment variables. Here’s how I configure CellarHQ:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ratpack {
  List<String> programArgs = HerokuUtils.extractDbProperties
    .apply(System.getenv("DATABASE_URL"))

  serverConfig {
    config -> config
      .baseDir(BaseDir.find())
      .props("app.properties")
      .yaml("db.yaml")
      .env()
      .sysProps()
      .args(programArgs.stream().toArray() as String[])
      .require("/cellarhq", CellarHQConfig)
      .require("/db", HikariConfig)
      .require("/metrics", DropwizardMetricsConfig)
      .require("/cookie", ClientSideSessionConfig)
  }

  bindings {
    module CommonModule
    module HikariModule
    module AuthenticationModule
    module CommonModule
    module ApiModule
    module WebappModule
    module HandlebarsModule
    module SessionModule
    module ClientSideSessionModule
    module DropwizardMetricsModule

    bindInstance Service, new Service() {
      @Override
      void onStart(StartEvent event) throws Exception {
        RxRatpack.initialize()
      }
    }

    bind ServerErrorHandler, ServerErrorHandlerImpl
    bind ClientErrorHandler, ClientErrorHandlerImpl
    bind DatabaseHealthcheck
  }

All configuration is added through the ServerConfig an requiring specific stanzas of configuration including cellarhq, db etc. An instance of each config object is added to the registry. This lets the bindings block stay very simple. Let’s examine the configuration line by line:

1
2
List<String> programArgs = HerokuUtils.extractDbProperties
    .apply(System.getenv("DATABASE_URL"))

Heroku provides an environment variable which has all the fields for the JDBC connection which needs to be parsed and added to the Ratpack configuration manually. This is a helper utility I gratuitously stole from Modern Java Web Development.

1
2
3
4
5
  serverConfig {
    config -> config
      .baseDir(BaseDir.find())
      .props("app.properties")
      .yaml("db.yaml")

This will load two configuration files, one is a properties file and one is a yaml file, from the ratpack BaseDir. You’ll want to define this by including a .ratpack file somewhere in your source tree. Mine is in src/ratpack.

1
2
3
.args(programArgs.stream().toArray() as String[])
.sysProps()
.env()

Now the herku specific configuration, system properties, environment variables are layered on top of the configuration files, in the order specified. System properties override configuration from app.properties and environment variables override those.

1
2
3
4
.require("/cellarhq", CellarHQConfig)
.require("/db", HikariConfig)
.require("/metrics", DropwizardMetricsConfig)
.require("/cookie", ClientSideSessionConfig)

Finally, specific configuration objects are built using data-binding from the specified properties. An instance of each of these classes is added to the registry and available just like any other object added to the registry.

Setting the configuration

Now let’s look at the configuration itself. In app.properties and db.yaml sensible defaults are configured for every configuration item that isn’t secret like AWS or Twitter API keys. Those items are always configured at runtime and stored some place not in source control.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
liquibase.changelog=migations.xml
liquibase.onerror.fail=true

metrics.jvmMetrics=true
metrics.jmx.enabled=true
metrics.csv.enabled=false
metrics.webSocket.reporterInterval=PT30S
metrics.webSocket.excludeFilter=.*(js|css|ico|woff|admin|login|pac4j).*
metrics.requestMetricGroups.update=update.*
metrics.requestMetricGroups.delete=delete.*

cookie.sessionCookieName = cellarhq_session
cookie.secretToken = secretTokenIsOverridenInProd

cellarhq.googleAnalyticsTrackingCode=UA-27709782-2
cellarhq.hostname=localhost:5050
cellarhq.s3StorageBucket=storage-local.cellarhq.com
cellarhq.environment=development
1
2
3
4
5
6
7
8
db:
  dataSourceClassName: org.postgresql.ds.PGSimpleDataSource
  username: cellarhq
  password: cellarhq
  dataSourceProperties:
    databaseName: cellarhq
    serverName: localhost
    portNumber: 15432

Overriding with system properties or env vars

The defaults above work great for running locally, but I do override some of them for tests. I use the gradle build to set environment variables or system properties. Here’s how I configure functional tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
task functionalTest(type: Test) {
  testClassesDir = sourceSets.functional.output.classesDir
  classpath = sourceSets.functional.runtimeClasspath

  systemProperty 'liquibase.changelog', rootProject.file('model/migrations/migrations.xml').canonicalPath
  systemProperty 'liquibase.schema.default', 'public'
  systemProperty 'liquibase.onerror.fail', true
  maxHeapSize '768m'

  if (System.getenv('SNAP_CI')) {
    environment 'RATPACK_DB__DATA_SOURCE_PROPERTIES__SERVER_NAME', System.getenv('SNAP_DB_PG_HOST')
    environment 'RATPACK_DB__DATA_SOURCE_PROPERTIES__PORT_NUMBER', System.getenv('SNAP_DB_PG_PORT')
    environment 'RATPACK_DB__DATA_SOURCE_PROPERTIES__DATABASE_NAME', 'app_test'
    environment 'RATPACK_DB__USER', System.getenv('SNAP_DB_PG_USER')
    environment 'RATPACK_DB__PASSWORD', System.getenv('SNAP_DB_PG_PASSWORD')
    environment 'RATPACK_CELLARHQ__AWS_ACCESS_KEY', System.getenv('AWS_ACCESS_KEY') ?: 'BAD_KEY'
    environment 'RATPACK_CELLARHQ__AWS_SECRET_KEY', System.getenv('AWS_SECRET_KEY') ?: 'BAD_KEY'
    environment 'RATPACK_CELLARHQ__TWITER_API_KEY', System.getenv('TWITTER_API_TOKEN') ?: 'BAD_KEY'
    environment 'RATPACK_CELLARHQ__TWITTER_API_SECRET', System.getenv('TWITTER_API_SECRET') ?: 'BAD_KEY'

    testLogging.showStandardStreams = true
  } else {
    environment 'RATPACK_DB__DATA_SOURCE_PROPERTIES__DATABASE_NAME', 'cellarhq_testing'
    if (project.hasProperty('awsAccessKey')) {
      environment 'RATPACK_CELLARHQ__AWS_ACCESS_KEY', project.awsAccessKey
      environment 'RATPACK_CELLARHQ__AWS_SECRET_KEY', project.awsSecretKey
      environment 'RATPACK_CELLARHQ__TWITER_API_KEY', project.twitterApiKey
      environment 'RATPACK_CELLARHQ__TWITTER_API_SECRET', project.twitterApiSecret
    }
  }
}

The build detects if it is running in a CI environment and maps configuration appropriately. Otherwise it sets a new database name and loads secrets from the gradle properties. I keep secrets in ~/.gradle/gradle.properties so they will never be added to source control accidentally.

There are some important things to notice here:

  • Environment variables start with RATPACK_, system properties with ratpack.
  • For env vars, TWO underscores are used to separate objects like CELLARHQ__
  • For env vars, ONE underscore is used to separate words that are built into camel cased field names, like AWS_ACCESS_KEY.
  • For system properties, the . is used for both object and field separators.

So, RATPACK_DB__DATA_SOURCE_PROPERTIES__DATABASE_NAME and ratpack.db.dataSourceProperties.databaseName are equivalent. Using system properties or environment variables is mostly a matter of preference.

What about lists?

I don’t have any properties which data-bind to lists, but this is possible in ratpack. Let’s say I had multiple S3 storage buckets configured, that could be configured in a properties file like this:

1
2
3
cellarhq.s3StorageBuckets[0]=storage-local.cellarhq.com
cellarhq.s3StorageBuckets[1]=storage-local.cellarhq.com
cellarhq.s3StorageBuckets[2]=storage-local.cellarhq.com

It could be done similarly with system properties. However, this DOES NOT WORK with environment variables. You cannot set items in a list using environment variable configuration.

Conclusion

Ratpack configuration is simple and elegant, and it is easy to layer environment specific configuration on top of defaults, I just thought there needed to be a few real life examples out there.