There comes a time when you want some form of functionality to run for every single request to your webserver. You might also want some more fine grained control and possibly run some slightly different logic for each individual route. This can be called middleware, filtering, interceptors, decorators, whatever you want to call it. Undertows composition gives you total control over how you handle this. You can build simple or complex route trees, deep trees or shallow trees. Some examples would be Access Logging, Exception Handling, gzipping, blocking, authorization, authentication, metrics / timing, rate limiting, caching and many more. Undertow supplies many of these out of the box.

Routes

/*
 * Notice how we have route level middleware here with the timed HttpHandler.
 */
private static final HttpHandler ROUTES = new RoutingHandler()
    .get("/textFast", timed("textFast", MiddlewareServer::textFast))
    .get("/textSlow", timed("textSlow", MiddlewareServer::textSlow))
    .get("/throwException", timed("throwException", MiddlewareServer::throwException))
    .get("/metrics", timed("metrics", MiddlewareServer::metrics))
    .setFallbackHandler(timed("notFound", MiddlewareServer::notFound))
;

Handlers

private static void textFast(HttpServerExchange exchange) {
    Exchange.body().sendText(exchange, "Fast!");
}

private static void textSlow(HttpServerExchange exchange) {
    // Sure whatever just sleep.
    try {
        Thread.sleep(300L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    Exchange.body().sendText(exchange, "Slow!!!");
}

private static void throwException(HttpServerExchange exchange) {
    throw new RuntimeException("Error, I hope you handled it!");
}

private static void notFound(HttpServerExchange exchange) {
    exchange.setStatusCode(404);
    Exchange.body().sendText(exchange, "Not Found");
}

private static void metrics(HttpServerExchange exchange) {
    Exchange.body().sendJson(exchange, Metrics.registry());
}

Middleware

This is where the magic happens! We added a few new classes feel free to follow the code in github its all there.

private static HttpHandler exceptionHandler(HttpHandler next) {
    return Handlers.exceptionHandler(next)
       .addExceptionHandler(Throwable.class, ExceptionHandlers::handleAllExceptions);
}

private static HttpHandler wrapWithMiddleware(HttpHandler handler) {
    /*
     * Undertow has I/O threads for handling inbound connections and non blocking IO.
     * If you are doing any blocking you should use the BlockingHandler. This
     * will push work into a separate Executor with customized thread pool
     * which is made for blocking work. I am blocking immediately here because I am lazy.
     * Don't block if you don't need to. Remember you can configure only certain routes block.
     * When looking at logs you can tell if you are blocking or not by the thread name.
     * I/O non blocking thread - [XNIO-1 I/O-{threadnum}] - You should NOT be blocking here.
     * Blocking task threads - [XNIO-1 task-{threadnum}] This pool is made for blocking.
     */
    return MiddlewareBuilder.begin(BlockingHandler::new)
                            .next(CustomHandlers::gzip)
                            .next(CustomHandlers::accessLog)
                            .next(CustomHandlers::statusCodeMetrics)
                            .next(MiddlewareServer::exceptionHandler)
                            .complete(handler);
}

Server

public static void main(String[] args) {
    // Wrapping routes with middleware here!
    SimpleServer server = SimpleServer.simpleServer(wrapWithMiddleware(ROUTES));
    server.start();
}

Logs

23:41:12.022 [XNIO-1 task-30] INFO com.stubbornjava.accesslog - 127.0.0.1 - - [12/Jan/2017:23:41:12 -0500] "GET /throwException HTTP/1.1" 500 42 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36"
23:41:12.221 [XNIO-1 task-31] INFO com.stubbornjava.accesslog - 127.0.0.1 - - [12/Jan/2017:23:41:12 -0500] "GET /throwException HTTP/1.1" 500 42 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36"
23:41:15.086 [XNIO-1 task-21] INFO com.stubbornjava.accesslog - 127.0.0.1 - - [12/Jan/2017:23:41:15 -0500] "GET /favicon.ico HTTP/1.1" 404 9 "http://localhost:8080/throwException" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36"
23:41:52.880 [XNIO-1 task-31] INFO com.stubbornjava.accesslog - 127.0.0.1 - - [12/Jan/2017:23:41:52 -0500] "GET /metrics HTTP/1.1" 200 1004 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36"

gzip

curl -v --compressed localhost:8080/metrics
*   Trying ::1...
* connect to ::1 port 8080 failed: Connection refused
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /metrics HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.49.1
> Accept: */*
> Accept-Encoding: deflate, gzip
>
< HTTP/1.1 200 OK
< Content-Encoding: gzip
< Connection: keep-alive
< Content-Type: application/json
< Content-Length: 1012
< Date: Fri, 13 Jan 2017 04:56:20 GMT

Metrics

Hit GET /metrics and woah! Push this data to statsd or cloudwatch and make some useful graphs or alarms. DropwizardMetrics probably even has a reporter already built for you. We can monitor fluctuations in status code rates or stats for individual routes.

{
  "version": "3.0.0",
  "gauges": {},
  "counters": {},
  "histograms": {},
  "meters": {
    "status.code.200": {
      "count": 92,
      "m15_rate": 25.747115560734112,
      "m1_rate": 28.062428714932278,
      "m5_rate": 28.256280991640015,
      "mean_rate": 39.9843206826378,
      "units": "events/minute"
    },
    "status.code.404": {
      "count": 49,
      "m15_rate": 33.32440534815085,
      "m1_rate": 16.92221744365673,
      "m5_rate": 28.960821933949145,
      "mean_rate": 20.288716297062063,
      "units": "events/minute"
    },
    "status.code.500": {
      "count": 32,
      "m15_rate": 76.34270318747376,
      "m1_rate": 27.26465224526377,
      "m5_rate": 63.55688467693468,
      "mean_rate": 17.258731172178305,
      "units": "events/minute"
    }
  },
  "timers": {
    "metrics": {
      "count": 11,
      "max": 306.412497,
      "mean": 9.846115333732579,
      "min": 1.879319,
      "p50": 2.1016,
      "p75": 2.783833,
      "p95": 6.784892999999999,
      "p98": 306.412497,
      "p99": 306.412497,
      "p999": 306.412497,
      "stddev": 45.87752194251882,
      "m15_rate": 0.32356973506686026,
      "m1_rate": 3.6573158975635676,
      "m5_rate": 0.9190025702482435,
      "mean_rate": 4.355696619792746,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "notFound": {
      "count": 49,
      "max": 52.264905999999996,
      "mean": 1.0456300019306812,
      "min": 0.369432,
      "p50": 0.584043,
      "p75": 0.736752,
      "p95": 1.273145,
      "p98": 1.620323,
      "p99": 14.762761999999999,
      "p999": 52.264905999999996,
      "stddev": 3.9050701815418183,
      "m15_rate": 2.632731129701274,
      "m1_rate": 14.557189323028163,
      "m5_rate": 6.7008561229401815,
      "mean_rate": 19.402677037911392,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "textFast": {
      "count": 33,
      "max": 3.3543689999999997,
      "mean": 0.8341323532906185,
      "min": 0.387693,
      "p50": 0.6643049999999999,
      "p75": 0.982436,
      "p95": 2.3764309999999997,
      "p98": 2.3764309999999997,
      "p99": 3.3543689999999997,
      "p999": 3.3543689999999997,
      "stddev": 0.5411243129002735,
      "m15_rate": 1.9944095414062617,
      "m1_rate": 7.989583466347278,
      "m5_rate": 4.925595501107286,
      "mean_rate": 13.06637838616076,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "textSlow": {
      "count": 48,
      "max": 308.47148,
      "mean": 303.76731141857067,
      "min": 300.7573,
      "p50": 303.578967,
      "p75": 305.30724599999996,
      "p95": 306.281167,
      "p98": 308.47148,
      "p99": 308.47148,
      "p999": 308.47148,
      "stddev": 1.8166335597552041,
      "m15_rate": 2.9604080623559144,
      "m1_rate": 15.975594713032923,
      "m5_rate": 7.619162286729776,
      "mean_rate": 19.006476987571602,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    },
    "throwException": {
      "count": 32,
      "max": 0.12914599999999998,
      "mean": 0.060840682082171124,
      "min": 0.028107999999999998,
      "p50": 0.0545,
      "p75": 0.074738,
      "p95": 0.114078,
      "p98": 0.114078,
      "p99": 0.12914599999999998,
      "p999": 0.12914599999999998,
      "stddev": 0.024935798577304682,
      "m15_rate": 2.007469708700337,
      "m1_rate": 13.92037262527053,
      "m5_rate": 5.347307213526083,
      "mean_rate": 12.671015244967208,
      "duration_units": "milliseconds",
      "rate_units": "calls/minute"
    }
  }
}