Route itself is a function that operates on a
RequestContext and returns a
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
akka.stream.Materializer, so that these don't have to be passed around manually.
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 is an opaque structure that represents possible results of evaluating a route. A
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
completeWith was used or a rejection that contains information about why the route could not handle the request.
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
Directive.route() as in
path("xyz").route() or to directives that directly take a variable number
of inner routes as argument like
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
call let the request pass through.
- Route 2 will run if
- Route 3 will run if
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.