LangInteger

Building Web Server With Akka HTTP

Philosophy of Akka HTTP quoted from their documentations:

The Akka HTTP modules implement a full server and clientside HTTP stack on top of akka-actor and akka-stream. It’s not a web-framework but rather a more general toolkit for providing and consuming HTTP-based services. While interaction with a browser is of course also in scope it is not the primary focus of Akka HTTP.

Akka HTTP was designed specifically as “not-a-framework”, not because we don’t like frameworks, but for use cases where a framework is not the right choice. Akka HTTP is made for building integration layers based on HTTP and as such tries to “stay on the sidelines”. Therefore you normally don’t build your application “on top of” Akka HTTP, but you build your application on top of whatever makes sense and use Akka HTTP merely for the HTTP integration needs.

Less is more, that’s why we choose Akka HTTP, the other functions that a framework provides like view templating, asset management, JavaScript and CSS generation/manipulation/minification, localization support, AJAX support, etc, are not necessary in our development.

Some framework may be taken into consideration:

Simple Case

Handle http request synchronizely with pattern matching:

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import scala.io.StdIn

object WebServer {

  def main(args: Array[String]) {
    implicit val system = ActorSystem()
    implicit val materializer = ActorMaterializer()
    // needed for the future map/flatmap in the end
    implicit val executionContext = system.dispatcher

    val requestHandler: HttpRequest => HttpResponse = {
      case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
        HttpResponse(entity = HttpEntity(
          ContentTypes.`text/html(UTF-8)`,
          "<html><body>Hello world!</body></html>"))

      case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
        HttpResponse(entity = "PONG!")

      case HttpRequest(GET, Uri.Path("/crash"), _, _, _) =>
        sys.error("BOOM!")

      case r: HttpRequest =>
        r.discardEntityBytes() // important to drain incoming HTTP Entity stream
        HttpResponse(404, entity = "Unknown resource!")
    }

    val bindingFuture = Http().bindAndHandleSync(requestHandler, "localhost", 8080)
    println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
    StdIn.readLine() // let it run until user presses return
    bindingFuture
      .flatMap(_.unbind()) // trigger unbinding from the port
      .onComplete(_ => system.terminate()) // and shutdown when done

  }
}

Handle http request asynchronizely with flexible DSL:

import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
import akka.http.scaladsl.server.{HttpApp, Route}

object Main1 extends HttpApp {
  
  protected def routes: Route =
    get {
      pathSingleSlash {
        complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<html><body>Hello world!</body></html>"))
      } ~
        path("ping") {
          complete("PONG!")
        } ~
        path("crash") {
          sys.error("BOOM!")
        }
    }

  startServer("localhost", 8082)
}

What’s Route

The “Route” is the central concept of Akka HTTP’s Routing DSL. Reading the definitation:

type Route = RequestContext => Future[RouteResult]

Route is a simple alias for a function turning a RequestContext into a Future[RouteResult]. When a route receives a requesr(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(…)
  • Fail the request by returning the value of `requestContext.fail(…) or by just throwing an exception
  • Do any kind of asynchronous processing and instantly return a Future[RouteResult] to be eventually completed later

RouteResult

RouteResult is a simple abstract data type(ADT) that models the possible non-error results of a 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
}

Composing Routes

Three basic operations for building more complex routes for simpler ones:

  • Route transformation: which delegates processing to another, ‘inner’ route but in procss changes some properties of either the incoming reqeust, 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 last point is achieved with the concatenation operator ~, which is an extension method that becomes available when you import akka.http.scaladsl.server.Directives._. The first two points are provided by so-called Directives of which a large number is already predefined by Akka HTTP and whic hyou can also easily create yourself. Directives deliver most of Akka HTTP’s power and flexibility.

Sealing a Route

Sealing means 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, which can be done by:

  • Bring rejection/exception handlers into implicit scope at the top-level
  • Supply handlers as arguments to handleRejections and handleException directives

What’s Directive

The Directive is a small building block used for creating arbitarily complete route structures. Diretives create Routes. Here is the primitive way of creating routes:

val helloWorldWithPrimitive: Route =
  ctx => ctx.complete("42")

val helloWorldWithPrimitive1: Route =
  _.complete("42")

which can be replaced with complete directive(import akka.http.scaladsl.server.Directives._ as a preRequisite):

val helloWorldWithDirective: Route =
  complete("42")

Let’s take things one step further with plain method:

val route: Route = { ctx =>
  if (ctx.request.method == HttpMethods.GET)
    ctx.complete("Received GET")
  else
    ctx.complete("Received something else")
}

which can be replaced using directive:

val route: Route =
  get {
    complete("Received GET")
  } ~
  complete("Received something else")

What directives do

  • Transform the incoming RequestContext before passing it onto its innner route (i.e. modify the requst)
  • Filter the RequestContext according to some logic, i.e. only pass on certain requests and reject others
  • Extract values from the RequestContext and make them available to its inner route as “extractions”
  • Chain some logic into the RouteResult future transformation chain(i.e. modify the response or rejection)
  • Complete the request

This means a Directive completely wraps the functionality of its inner route and can apply arbitrarily complex transformations, both (or either) on the request and on the response side.

Composing Directives

Concat directives with concat combinator or using ~ operator:

`concat(a, b, c)` is equals to `a ~ b ~ c`

Rejections

The concept of rejecitons is used by Akka HTTP for maintaining a more functional overall architecture and in order to be able to properly handle all kinds of error scenarios.

A rejection encapsulates a specific reason why a route was not able to handle a request. It is modeled as an object of type Rejection. Akka HTTP comes with a set of predefined rejections, which are used by many predefined directives.

These situations can be summarized to rejectons:

  • AuthorizationFailedRejection
  • ValidationRejection
  • MethodRejection
  • NotFound

Exception Handling

Exceptions thrown during route execution bubble up through the route structure to the next exclosing handleException directive or the top of your route structure.

trait ExceptionHandler extends PartialFunction[Throwable, Route]

ExceptionHandler is a partial function, it can choose which exceptions it would like to handle and which not. Unhandled exceptions will simply continue to bubble up in the route structure. At the root of the route tree any still unhandled exception will be dealt with by the top-level handler which always handles all exceptions.

Case Class Extraction

Json is used to transfer data between front-end and back-end, so the topic is about marshalling/unmarshalling json.

Spray Json

With dependency:

"com.typesafe.akka" %% "akka-http-spray-json" % httpVersion,

Request Parameter Validation

  • todo

Reference

Magnet Pattern
The Reactive Manifesto
Call super constructor in Java
How to get JSON as string data from HttpResponse in Akka HTTP
What is the difference between Rejection and Exception in akka-http