1. Introduction

In this tutorial, we’ll see how to read the response body as String when using Akka HTTP to build an HTTP-based API. We’ll first take a high-level view of Akka HTTP and then look at two ways of reading the body of a response as String: strict requests and unmarshalling.

2. Akka HTTP

Akka HTTP is an Akka module used to implement a client/server HTTP stack on top of Akka’s actors and stream modules. It comes with a DSL (Domain-Specific Language) to build HTTP endpoints and a client API to perform HTTP calls.

Akka HTTP was designed with streaming in mind. Both requests and responses stream through the server to reduce memory consumption and to provide back-pressuring so that the remote client will not produce data faster than the server can consume it.

2.1. Dependencies

As Akka HTTP depends on Akka actor and stream, we’ll import several dependencies into our project:

val AkkaVersion = "2.8.0"
val AkkaHttpVersion = "10.5.0"
libraryDependencies ++= Seq(
    "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion,
    "com.typesafe.akka" %% "akka-stream" % AkkaVersion,
    "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
)

2.2. A Simple Route

In this tutorial, we’ll use the routing DSL to build a simple HTTP server, and then we’ll see how to read responses as String.

A route is simply an endpoint that can be called after binding it to a specific host and port.

First, let’s create a simple route to experiment with:

object SimpleRouter {
  val route: Route = path("hello") {
    get {
      complete(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Hello, world!"))
    }
  }
}

In the example above, we used Akka HTTP’s DSL to define an endpoint. The scope is that of an object, SimpleRouter, so that we can reference the route simply by SimpleRouter.route. We’ve also defined the path as /hello and that we only accept GET requests. Lastly, we use complete to define the response we’ll send.

Now, let’s see how to make our route reachable at http://localhost:8080:

object App {
  def main(args: Array[String]): Unit = {
    implicit val system: ActorSystem[Nothing] =
      ActorSystem(Behaviors.empty, "simple-system")
    implicit val executionContext: ExecutionContextExecutor =
      system.executionContext

    val binding = Http().newServerAt("localhost", 8080).bind(SimpleRouter.route)

    binding
      .flatMap(_.unbind())
      .onComplete(_ => system.terminate())
  }
}

The core of the example is the value we assign to binding: Http().newServerAt(“localhost”, 8080).bind(SimpleRouter.route). There, we specify the host as localhost and the port as 8080. Also, we tell Akka HTTP which route to make available at that host and port, SimpleRouter.route, in this case.

However, for that line of code to compile, we need a couple of implicit parameters. In particular, as Akka HTTP relies heavily on Akka actors, we need an ActorSystem. Furthermore, since we’re using the typed version of Akka, we need a type parameter for ActorSystem, Nothing in the example. This means that the system has no behavior, and we just need it to provide an execution context. The next thing we declare as an implicit is an ExecutionContextExecutor to be able to call flatMap and onComplete on the binding Future.

Lastly, we need to unbind the server when the application terminates so that the port will be released. In the end, we also terminate the ActorSystem. We can do that in an asynchronous manner by leveraging  Future::flatMap() and Future::onComplete().

3. Response Body as String

Once we start our simple HTTP server, we can navigate to http://localhost:8080/hello to see “Hello, world!” printed out. Let’s see how we can interact with the server and how to read the response as String.

3.1. Strict Request

Due to the streaming nature of Akka HTTP, the content of the response is treated as a stream in the library. Let’s see how we can work around that:

val responseFuture = 
  Http().singleRequest(HttpRequest(uri = "http://localhost:8080/hello"))
val timeout = 300.millis
val responseAsString = Await.result(
  responseFuture
    .flatMap { resp => resp.entity.toStrict(timeout)}
    .map { strictEntity => strictEntity.data.utf8String },
  timeout
)

assert(responseAsString == "Hello, world!")

First, we make an HTTP call to our simple endpoint via Http().singleRequest(). This gives us a Future[HttpResponse]. If we were writing a real application, we would work on the Future using methods such as map, flatMap, and so on. For now, we’ll wait for the Future to complete by using Await:result(), with a timeout of 300 milliseconds. Nonetheless, we can manipulate responseFuture to extract the content of the response.

Next, we extract the body of the response (resp.entity). Here, we must use HttpEntity::toStrict() to work around the streaming nature of Akka HTTP. HttpEntity::toStrict() requires another implicit parameter, an instance of Materializer. A Materializer is a component that actually “runs” the stream, producing a value as an output. Hence, we are basically draining the stream containing the body of the response and loading it all in memory. Since this is an asynchronous operation, we also need to specify a timeout.

In the example, we don’t define an instance of Materializer explicitly, as we did for ActorSystem and ExecutionContextExecutor. This is because the Materializer class provides us with an implicit conversion from ActorSystem to Materializer. Therefore, Scala takes care of getting the Materializer from the ActorSystem for us.

Once we have the response body in memory, we can extract the data and convert it to a String (strictEntity.data.utf8String). The first part, strictEntity.data, returns an instance of ByteString, which is basically a sequence of bytes.

Once the corresponding Future completes, resposeAsString will contain a representation of the response encoded in UTF-8.

3.2. Unmarshalling

Akka HTTP provides, out of the box, predefined unmarshallers for a number of types, including String, Boolean, Int, and all the other numeric data types.

Let’s see how we can do use that for String:

val responseFuture = 
  Http().singleRequest(HttpRequest(uri = "http://localhost:8080/hello"))
val timeout = 300.millis
val responseAsString = Await.result(
  responseFuture.flatMap(resp => Unmarshal(resp.entity).to[String]),
  timeout
)

assert(responseAsString == "Hello, world!")

Again, we first make an HTTP call to our simple endpoint.

The code to read the response is now much simpler than before, as we won’t have to take care of low-level details related to the streaming nature of Akka HTTP. We just need to use the Unmarshal::to() method to turn an instance of HttpEntity (that is resp.entity) into a String. We wait for the Future to complete using Await::result().

As a matter of fact, the only reference to Akka Streams in the second example is the need for an implicit Materializer in the call to Unmarshal::to(). The implicit conversion from ActorSystem takes care of that for us.

4. Conclusion

In this article, we saw how to read the body of a response as String when using Akka HTTP. We first created a very simple HTTP server and then used it to compare two methods,  “strict” entities and unmarshalling. In particular, the latter is the recommended way to proceed, as it hides many low-level details about the streaming nature of Akka HTTP.

As usual, you can find the code over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.