Migrating from Spray to Akka HTTP

We cover the trickiest changes when it comes to migrating from Spray to Akka HTTP.

photo of Nikita Melkozerov
Nikita Melkozerov

Software Engineer – Merchant Orders

Posted on May 03, 2016
Tags:

Spray is a well-known HTTP library in the Scala ecosystem. It was released in 2011, and since then it’s been widely used by the Scala community. It was recently announced that Spray would be replaced with Akka HTTP, thus cementing Akka HTTP as the successor of Spray. It’s maintained by Lightbend and it's been recommended that users migrate to it soon.

However, migration from one major version of a library to another is not an easy task. Very often it requires you to spend some time reading the source code in order to figure out how to use certain features, as well as how to migrate existing logic.

This post will demonstrate what changes should be applied in order to migrate your app from Spray to Akka HTTP. The following steps don’t have a particular order, as it depends on which areas need to be rewritten.

Packages

In order to have the latest Akka HTTP packages, all previous Spray dependencies now need to be replaced by the following:

 "com.typesafe.akka" %% "akka-http-core" % 2.4.3
 "com.typesafe.akka" %% "akka-http-experimental" % 2.4.3
 "com.typesafe.akka" %% "akka-http-testkit" % 2.4.3 % "test"

HttpService

Spray’s HttpService has been removed. Use Http class and pass your routes to the bindAndHandle method. For example:

Before:

val service = system.actorOf(Props(new HttpServiceActor(routes)))
IO(Http)(system) ! Http.Bind(service, "0.0.0.0", port = 8080)

After:

Http().bindAndHandle(routes, "0.0.0.0", port = 8080)

Marshalling

Marshaller.of can be replaced with Marshaller.withFixedContentType. See below:

Before:

Marshaller.of[JsonApiObject](`application/vnd.api+json`) { (value, contentType, ctx) =>
      ctx.marshalTo(HttpEntity(contentType, value.toJson.toString))
}

After:

Marshaller.withFixedContentType(`application/vnd.api+json`) { obj =>
      HttpEntity(`application/vnd.api+json`, obj.toJson.compactPrint)
}

Akka HTTP marshallers support content negotiation, so you don't have to specify the content type when creating one “super” marshaller from other marshallers:

Before:

ToResponseMarshaller.oneOf(
      `application/vnd.api+json`,
      `application/json`
    )(
      jsonApiMarshaller,
      jsonMarshaller
}

After:

Marshaller.oneOf(
      jsonApiMarshaller,
      jsonMarshaller
    )

Unmarshalling

The example below shows one way that an Unmarshaller might be rewritten:

Before:

Unmarshaller[Entity](`application/vnd.api+json`) {
      case HttpEntity.NonEmpty(contentType, data) =>
data.asString.parseJson.convertTo[Entity]
    }

After:

Unmarshaller.stringUnmarshaller.forContentTypes(`application/vnd.api+json`).map(_.parseJson.convertTo[Entity])

MediaTypes

MediaType.custom can be replaced with specific methods in MediaType object.

Before:

MediaType.custom("application/vnd.acme+json")

After:

MediaType.applicationWithFixedCharset("application/vnd.acme+json", HttpCharsets.`UTF-8`)

Rejection Handling

RejectionHandler now uses a builder pattern – see the example below:

Before:

def rootRejectionHandler = RejectionHandler {
    case Nil => {
      requestUri { uri =>
        logger.error("Route: {} does not exist.", uri)
        complete((NotFound, mapErrorToRootObject(notFoundError)))
      }
 case AuthenticationFailedRejection(cause, challengeHeaders) :: _ => {
      logger.error(s"Request is rejected with cause: $cause")
      complete((Unauthorized, mapErrorToRootObject(unauthenticatedError)))
      }

}

After:

RejectionHandler
    .newBuilder()
    .handle {
      case AuthenticationFailedRejection(cause, challengeHeaders) =>
        logger.error(s"Request is rejected with cause: $cause")
        complete((Unauthorized, mapErrorToRootObject(unauthenticatedError)))
.handleNotFound { ctx =>
      logger.error("Route: {} does not exist.", ctx.request.uri.toString())
      ctx.complete((NotFound, mapErrorToRootObject(notFoundError)))
    }
.result() withFallback RejectionHandler.default

Client

The Spray-client pipeline was removed. Use Http’s singleRequest method instead:

Before:

val pipeline: HttpRequest => Future[HttpResponse] = (addHeader(Authorization(OAuth2BearerToken(accessToken))) ~> sendReceive)
 val patch: HttpRequest = Patch(uri, object))

pipeline(patch).map { response     …
}

After:

val request = HttpRequest(
                method = PATCH,
                uri = Uri(uri),
                headers = List(Authorization(OAuth2BearerToken(accessToken))),
                entity = HttpEntity(MediaTypes.`application/json`, object)
              )

              http.singleRequest(request).map {
            case … => …
            …

}

Headers

All HTTP headers have been moved to the akka.http.scaladsl.model.headers._ package.

Form fields and file upload

With the streaming nature of http entity, it’s important to have a strict http entity before accessing multiple form fields or use file upload directives. One solution might be using next directive before working with form fields:

val toStrict: Directive0 = extractRequest flatMap { request =>
      onComplete(request.entity.toStrict(5.seconds)) flatMap {
        case Success(strict) =>
          mapRequest( req => req.copy(entity = strict))
        case _ => reject
      }
    }

And one can use it like this:

toStrict {
    formFields("name".as[String]) { name =>
        ...
    }
}

While this list isn’t an exhaustive collection of all the changes you need to do, it covers the trickiest ones that exist. One major drawback of Akka HTTP is that it’s not as mature as Spray, and it’s performance is not optimised yet. Users may also notice a lack of documentation for some cases.

Having said that, it would be a good idea to keep the above issues in mind during this process. Happy migration!



Related posts