Migrating a Project from Spray to Akka-http: General Tips and Reference

Last updated:

WIP Alert This is a work in progress. Current information is correct but more content will probably be added in the future.

Migrating an app built in Spray into Akka-http is supposed to be easy but there's a couple of things that might cause you some pain.

The following are a couple of things to watch out for:

For a complete akka-http seed project view akka-http-docker-seed project on github

Materializers

Lots of things that behave differently are related to the fact that akka-http is built on top of akka-streams.

Some operations (such as creating actors) now require that a Materializer be in scope in order to work.

You can simply add one like this:

import akka.stream.ActorMaterializer

object MyScope{
  implicit val mat = ActorMaterializer()

  // do stuff like create actors

}

Standalone Requests using the Http Client

It's somewhat simpler now.

Here's an example of how to write a simple async Http request using the default Http client:

import akka.http.scaladsl._
import akka.http.scaladsl.model._

// just for this example, you should be using your system's dispatcher instead
import ExecutionContext.Implicits.global

val url = "http://path.to.example.com/foo/bar"
val req = HttpRequest(method = HttpMethods.GET, uri = url)

Http().singleRequest(req).map{ response =>
    // deal with response
}

Extracting the Body Contents / Entity from an Http Request or Response

Like I said before, since akka-http is built on top of akka-stream, some things get treated like streams, which can cause a little bit of extra code if you just want to resolve it quickly.

To extract a request/response body contents you need to unwrap the stream, which returns a Future:

It's also possible (and more convenient) to use a Marshaller to turn your entity into a model or a String but it also operates asynchronously (i.e. returns a Future)

import akka.http.scaladsl._
import akka.http.scaladsl.model._
import scala.concurrent.duration._

// just for this example, you should be using your system's dispatcher instead
import ExecutionContext.Implicits.global

// for this you also need a materializer in scope
implicit val mat = ActorMaterializer()

val url = "http://path.to.example.com/foo/bar"
val req = HttpRequest(method = HttpMethods.GET, uri = url)

// this is the maximum time you will wait for the stream before unpacking
val streamTimeout = 1.seconds

Http().singleRequest(req).map{ response =>
  response.entity.toStrict(streamTimeout).map { materializedStream =>
    val responseBody = materializedStream.data.utf8String

    if(response.status.isSuccess){
        println(s"SUCCESS! Response was: $responseBody")
    }else{
        println("FAILURE!")
    }
  }
}

No more dynamic directive

In Spray, any routes that weren't explicitly wrapped in a dynamic{} block would be automatically cached and not run for every request.

In Akka-http, anything that is within a complete{} block is automatically re-evaluated for every request (which is the expected result, in my opinion) rather than eagerly.

For more info on this, see this github issue

//imports ommitted

val routes = {
  path("somepath"){
    get{
      myInnerRoute
    }
  }
}

def myInnerRoute: StandardRoute = {
  val foo = "bar"

  complete{
    // no need for dynamic directive
    operationThatReturnsAFuture
  }
}

You Need to Process Response Entities even if you Wish to Ignore them

Akka-http is stream based and this makes HTTP Requests get stuck if you send one and never consume its result (entity.dataBytes). This is needed even if you don't need to know about it (e.g. in cases you just want to check whether a response isSuccess or not).

If you forget to do this, you'll probably get an error message like this: akka.stream.BufferOverflowException: Exceeded configured max-open-requests value of [32]

In the future, this may be changed so that you are warned if you forget to do this. See this, this and this github issue for more information.

Example: sending an HTTP request and discarding its result so that the HTTP request doesn't get blocked waiting to be consumed:


val postBody = """{ "foo": "bar" }"""

val req = HttpRequest(method = HttpMethods.POST, uri = Uri("http://example.com"), entity = HttpEntity(ContentTypes.`application/json`, postBody))

// suppose you just want to check whether the request was a success or not:
Http().singleRequest(req).map{ response =>
  if(response.status.isSuccess){
    console.log("success!")
  } else{
    console.log("failure..") 
  }

  // DON'T FORGET to DISCARD THE RESPONSE ENTITY!
  // OTHERWISE YOU MAY HIT THE MAXIMUM NUMBER OF OPEN HTTP REQUESTS IN YOUR APP!
  response.discardEntityBytes()

}

See also

Dialogue & Discussion