Routes
The “Route” is the central concept of Apache Pekko 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 type
function
turning a RequestContext
RequestContext
into a Future[RouteResult]
CompletionStage<RouteResult>
.
type Route = RequestContext => Future[RouteResult]
It’s a simple alias for a function turning a RequestContext
RequestContext
into a Future[RouteResult]
.
A Route
Route
itself is a function that operates on a RequestContext
RequestContext
and returns a RouteResult
RouteResult
. The RequestContext
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 Materializer
Materializer
, so that these don’t have to be passed around manually.
Generally when a route receives a request (or rather a RequestContext
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
Future[RouteResult]
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
Route
can be “sealed” using Route.seal
, which relies on the in-scope RejectionHandler
and ExceptionHandler
ExceptionHandler
instances to convert rejections and exceptions into appropriate HTTP responses for the client. Sealing a Route is described more in detail later.
Using Route.toFlow
or Route.toFunction
a Route
Route
can be lifted into a handler Flow
Flow
or async handler function to be used with a bindAndHandleXXX
call from the Core Server API.
Note: There is also an implicit conversion from Route
Route
to Flow<HttpRequest, HttpResponse, Unit>
Flow[HttpRequest, HttpResponse, Unit]
defined in the RouteResult
RouteResult
companion, which relies on Route.toFlow
.
RequestContext
The request context wraps an HttpRequest
HttpRequest
instance to enrich it with additional information that are typically required by the routing logic, like an ExecutionContext
, Materializer
Materializer
, LoggingAdapter
LoggingAdapter
and the configured RoutingSettings
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
RequestContext
itself is immutable but contains several helper methods which allow for convenient creation of modified copies.
RouteResult
RouteResult
RouteResult
is a simple algebraic data type (ADT) that models the possible non-error results of a Route
Route
. It is defined as such:
sealed trait RouteResult
object RouteResult {
final case class Complete(response: HttpResponse) extends RouteResult
final case class Rejected(rejections: immutable.Seq[Rejection]) extends RouteResult
}
Usually you don’t create any RouteResult
RouteResult
instances yourself, but rather rely on the pre-defined RouteDirectives (like complete, reject or redirect) or the respective methods on the RequestContext instead.
Composing Routes
There are three basic operations we need for building more complex routes from simpler ones:
- Route transformation, which delegates processing to another, “inner” route but in the process changes some properties of either the incoming request, the outgoing response or both
- Route filtering, which only lets requests satisfying a given filter condition pass and rejects all others
- Route chaining, which tries a second route if a given first one was rejected
The first two points are provided by so-called Directives of which a large number is already predefined by Apache Pekko HTTP and is extensible with user code.
The last point is achieved with the concat
method.
Directives deliver most of Apache Pekko HTTP’s power and flexibility.
The Routing Tree
Essentially, when you combine directives and custom routes via the concat
method, 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 {
concat(
b {
concat(
c {
... // route 1
},
d {
... // route 2
},
... // route 3
)
},
e {
... // route 4
}
)
}
import static org.apache.http.javadsl.server.Directives.*;
Route route =
directiveA(concat(() ->
directiveB(concat(() ->
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
A sealed route has these properties:
- The result of the route will always be a complete response, i.e. the result of the future is a
Success(RouteResult.Complete(response))
, never a failed future and never a rejected route. - Consequently, no route alternatives will be tried that were combined with this route.
As described in Rejections and Exception Handling, there are generally two ways to handle rejections and exceptions.
- Bring rejection/exception handlers
into implicit scope at the top-level
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.
Route.seal() method to modify HttpResponse
In application code, unlike test code, you don’t need to use the Route.seal()
method to seal a route. As long as you bring implicit rejection and/or exception handlers to the top-level scope, your route is sealed.
However, you can use Route.seal()
to perform modification on HttpResponse
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.
- Scala
-
source
val route = respondWithHeader(RawHeader("special-header", "you always have this even in 404")) { Route.seal( get { pathSingleSlash { complete { "Captain on the bridge!" } } }) }
- Java
-
source
public class RouteSealExample extends AllDirectives { public static void main(String[] args) { RouteSealExample app = new RouteSealExample(); app.runServer(); } public void runServer() { ActorSystem system = ActorSystem.create(); Route sealedRoute = get(() -> pathSingleSlash(() -> complete("Captain on the bridge!"))).seal(); Route route = respondWithHeader( RawHeader.create("special-header", "you always have this even in 404"), () -> sealedRoute); final Http http = Http.get(system); final CompletionStage<ServerBinding> binding = http.newServerAt("localhost", 8080).bind(route); } }
Converting routes between Java and Scala DSLs
In some cases when building reusable libraries that expose routes, it may be useful to be able to convert routes between their Java and Scala DSL representations. You can do so using the asScala
method on a Java DSL route, or by using an RouteAdapter
to wrap an Scala DSL route.
Converting Scala DSL routes to Java DSL:
- Scala
-
source
import org.apache.pekko val scalaRoute: pekko.http.scaladsl.server.Route = pekko.http.scaladsl.server.Directives.get { pekko.http.scaladsl.server.Directives.complete("OK") } val javaRoute: pekko.http.javadsl.server.Route = pekko.http.javadsl.server.directives.RouteAdapter.asJava(scalaRoute)
- Java
-
source
scala.Function1< org.apache.pekko.http.scaladsl.server.RequestContext, scala.concurrent.Future<org.apache.pekko.http.scaladsl.server.RouteResult>> scalaRoute = someRoute(); org.apache.pekko.http.javadsl.server.Route javaRoute = RouteAdapter.asJava(scalaRoute);
Converting Java DSL routes to Scala DSL:
- Scala
-
source
import org.apache.pekko val javaRoute = pekko.http.javadsl.server.Directives.get(new Supplier[pekko.http.javadsl.server.Route] { override def get(): Route = pekko.http.javadsl.server.Directives.complete("ok") }) // Remember that Route in Scala is just a type alias: // type Route = RequestContext => Future[RouteResult] val scalaRoute: pekko.http.scaladsl.server.Route = javaRoute.asScala
- Java
-
source
Route javaRoute = Directives.get(() -> Directives.complete("okey")); scala.Function1<RequestContext, Future<RouteResult>> scalaRoute = javaRoute.asScala();