Routes
Loading

Routes

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.

RequestContext

The RequestContext achieves two goals: it allows access to request data and it is a factory for creating a RouteResult. A user-defined handler (see Handlers) that is usually used at the leaf position of the route tree receives a RequestContext, evaluates its content and then returns a result generated by one of the methods of the context.

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, it can be an eventual result, i.e. a Future<RouteResult if completeWith was used 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". Such an aspect can be

  • 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

akka-http defines a library of predefined Directives and routes for all the various aspects of dealing with HTTP requests and responses.

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 either to Directive.route() as in path("xyz").route() or to directives that directly take a variable number of inner routes as argument like get().

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:

val route =
  a.route(
    b.route(
      c.route(
        ... // route 1
      ),
      d.route(
        ... // route 2
      ),
      ... // route 3
    ),
    e.route(
      ... // 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.

Contents