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
andc
all let the request pass through. - Route 2 will run if
a
andb
pass,c
rejects andd
passes. - Route 3 will run if
a
andb
pass, butc
andd
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.
- Pass rejection/exception handlers to the
seal()
method of theRoute
- Supply handlers as arguments to handleRejections and handleExceptions directives
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);
}
}