Routes

The “Route” is the central concept of Akka HTTP’s Routing DSL. All the structures you build with the DSL, no matter whether they consists of a single line or span several hundred lines, are functions turning a RequestContext into a CompletionStage<RouteResult>.

A Route itself is a function that operates on a RequestContext and returns a RouteResult. The RequestContext is a data structure that contains the current request and auxiliary data like the so far unmatched path of the request URI that gets passed through the route structure. It also contains the current ExecutionContext and akka.stream.Materializer, so that these don’t have to be passed around manually.

Generally when a route receives a request (or rather a RequestContext for it) it can do one of these things:

  • Complete the request by returning the value of requestContext.complete(...)
  • Reject the request by returning the value of requestContext.reject(...) (see Rejections)
  • Fail the request by returning the value of requestContext.fail(...) or by just throwing an exception (see Exception Handling)
  • Do any kind of asynchronous processing and instantly return a CompletionStage<RouteResult> to be eventually completed later

The first case is pretty clear, by calling complete a given response is sent to the client as reaction to the request. In the second case “reject” means that the route does not want to handle the request. You’ll see further down in the section about route composition what this is good for.

A Route can be “sealed” using Route.seal, which relies on the in-scope RejectionHandler and ExceptionHandler instances to convert rejections and exceptions into appropriate HTTP responses for the client.

Using Route.handlerFlow or Route.asyncHandler a Route can be lifted into a handler Flow or async handler function to be used with a bindAndHandleXXX call from the Low-Level Server-Side API.

RequestContext

The request context wraps an HttpRequest instance to enrich it with additional information that are typically required by the routing logic, like an ExecutionContext, Materializer, LoggingAdapter and the configured RoutingSettings. It also contains the unmatchedPath, a value that describes how much of the request URI has not yet been matched by a Path Directive.

The RequestContext itself is immutable but contains several helper methods which allow for convenient creation of modified copies.

RouteResult

The RouteResult is an opaque structure that represents possible results of evaluating a route. A RouteResult can only be created by using one of the methods of the RequestContext. A result can either be a response, if it was generated by one of the completeX methods, or a rejection that contains information about why the route could not handle the request.

Composing Routes

Routes are composed to form the route tree in two principle ways.

A route can be wrapped by a “Directive” which adds some behavioral aspect to its wrapped “inner route”. In the Java DSL, a Directive is a method that returns a Route. In many cases, a Directive method will have an inner route argument that is invoked when its semantics decide to do so, e.g. when a URL path is matched.

Example topics for directives include:

  • filtering requests to decide which requests will get to the inner route
  • transforming the request before passing it to the inner route
  • transforming the response (or more generally the route result) received from the inner route
  • applying side-effects around inner route processing, such as measuring the time taken to run the inner route

The other way of composition is defining a list of Route alternatives. Alternative routes are tried one after the other until one route “accepts” the request and provides a response. Otherwise, a route can also “reject” a request, in which case further alternatives are explored. Alternatives are specified by passing a list of routes to to RouteDirectives.route().

The Routing Tree

Essentially, when you combine routes via nesting and alternative, you build a routing structure that forms a tree. When a request comes in it is injected into this tree at the root and flows down through all the branches in a depth-first manner until either some node completes it or it is fully rejected.

Consider this schematic example. In place of directiveA, directiveB, etc., you can just imagine any of the available directives, e.g. matching a particular path, header or request parameter.:

import static akka.http.javadsl.server.Directives.*;

Route route =
  directiveA(route(() ->
    directiveB(route(() ->
      directiveC(
        ... // route 1
      ),
      directiveD(
        ... // route 2
      ),
      ... // route 3
    )),
    directiveE(
      ... // route 4
    )
  ));

Here five directives form a routing tree.

  • Route 1 will only be reached if directives a, b and c all let the request pass through.
  • Route 2 will run if a and b pass, c rejects and d passes.
  • Route 3 will run if a and b pass, but c and d reject.

Route 3 can therefore be seen as a “catch-all” route that only kicks in, if routes chained into preceding positions reject. This mechanism can make complex filtering logic quite easy to implement: simply put the most specific cases up front and the most general cases in the back.

Sealing a Route

As described in Rejections and Exception Handling, there are generally two ways to handle rejections and exceptions.

In the first case your handlers will be “sealed”, (which means that it will receive the default handler as a fallback for all cases your handler doesn’t handle itself) and used for all rejections/exceptions that are not handled within the route structure itself.

Modify HttpResponse from a sealed Route

You can use Route class’s seal() method to perform modification on HttpResponse from the route. For example, if you want to add a special header, but still use the default rejection handler, then you can do the following. In the below case, the special header is added to rejected responses which did not match the route, as well as successful responses which matched the route.

public class RouteSealExample extends AllDirectives {

  public static void main(String [] args) throws IOException {
    RouteSealExample app = new RouteSealExample();
    app.runServer();
  }

  public void runServer(){
    ActorSystem system = ActorSystem.create();
    final ActorMaterializer materializer = ActorMaterializer.create(system);

    Route sealedRoute = get(
      () -> pathSingleSlash( () ->
        complete("Captain on the bridge!")
      )
    ).seal(system, materializer);

    Route route = respondWithHeader(
      RawHeader.create("special-header", "you always have this even in 404"),
      () -> sealedRoute
    );

    final Http http = Http.get(system);
    final Flow<HttpRequest, HttpResponse, NotUsed> routeFlow = route.flow(system, materializer);
    final CompletionStage<ServerBinding> binding = http.bindAndHandle(routeFlow, ConnectHttp.toHost("localhost", 8080), materializer);
  }
}