Migration Guide from Spray

Akka HTTP is the successor for spray. With the first non-experimental release of Akka HTTP, spray has reached its end-of-life. Akka HTTP is a reimplementation of HTTP based on akka-stream (former spray-can) which adds streaming support on all levels. The popular high-level routing DSL (former spray-routing) has mostly been kept but was made more consistent and simplified where possible. While underlyings have changed a lot, many of the high-level features and syntax of the routing DSL have only changed superficially so that code can hopefully be converted with little effort.

Major changes

Streams everywhere

Akka HTTP offers an API based on streams where spray offered an API based on actor messaging. This has important consequences.

Streaming support is needed to handle request and response entities (or bodies) in a streaming fashion, i.e. being able to access the incoming bytes while they come in from the network without having to buffer a potentially big request or response in memory. The same is valid for sending out request or response data entities. In the model, the streaming underlyings can be seen in the HttpEntityHttpEntity type which now has subclasses that allow to specify a Source<ByteString, ?>Source[ByteString, _] to provide or consume entity data.

In spray, you could configure spray-can to send out HttpRequestPart and HttpResponsePart messages to receive a request or a response in a streaming fashion. The default case was for spray to collect the full entity in memory and send it out as a ByteStringByteString as part of the request or response entity object.

In Akka HTTP, handling streaming data is mandatory. When you receive a HttpRequestHttpRequest on the server-side or an HttpResponseHttpResponse, in the default case it will contain a streamed entity as the entity field which you are required to consume. Otherwise, a connection might be stuck (at least until timeouts kick in). See Implications of the streaming nature of Request/Response Entities.

In the implementation, Akka HTTP makes heavy use of streams as well with the occasional fallback to actors.

New module structure

The number of modules has been reduced. Here’s an approximate mapping from spray modules to new modules:

  • spray-util, spray-http, spray-can => akka-http-core
  • spray-routing => akka-http
  • spray-client => parts of high-level client support is now provided via Http().singleRequest, other is not yet implemented (see also #113)
  • spray-caching => akka-http-caching (since version 10.0.11, more information here: Documentation)

Package name changes

Classes can now be found in new packages:

  • the model can be found in akka.http.scaladsl.model
  • headers can be found in akka.http.scaladsl.model.headers._
  • the routing DSL can be found in akka.http.scaladsl.server._

Routing DSL not based on shapeless any more

To simplify using Akka HTTP together with other libraries that require shapeless, the routing DSL in Akka HTTP does not depend on shapeless any more. Instead, we support a light-weight replacement that models heterogeneous lists with tuples. This will not affect you as long as you haven’t written any generic directives. The implicit magic in the background that powers directives will - in user code - work as before.

Internally, the type aliases for DirectiveX have changed:

spray Akka HTTP
Directive0 Directive[HNil] Directive[Tuple0]
Directive1[T] Directive[T :: HNil] Directive[Tuple1[T]]
Directive2[T1, T2] Directive[T1 :: T2 :: HNil] Directive[Tuple2[T1, T2]]

Support for Java

All APIs are also available for Java. See everything under the akka.http.javadsl package.

Other changes

Changes in Route type

Route type has changed from Route = RequestContext => Unit to Route = RequestContext => Future[RouteResult]. Which means that now we must complete the Request inside the controller and we can’t simply pass the request to another Actor and complete it there. This has been done intentionally, because in Spray it was easy to forget to complete requests but the code would still compile.

The following article mentions a few ways for us to complete the request based on processing outside the controller: CodeMonkey blog - Actor per Request with Akka HTTP

This article was written by Johan Andrén, a member of the akka-http team.

Changes in Marshalling

Marshaller.of can be replaced with Marshaller.withFixedContentType.

Was:

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

Replace with:

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

Akka HTTP marshallers support content negotiation, now it’s not necessary to specify 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
)

Changes in Unmarshalling

Akka Http contains a set of predefined unmarshallers. This means that scala code like this:

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

needs to be changed into:

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

Changes in MediaTypes

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

Was:

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

Replace with:

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

Changes in Rejection Handling

RejectionHandler now uses a builder pattern – see the example:

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)

Changes in HTTP Client

The Spray-client pipeline was removed. Http’s singleRequest should be used instead of sendReceive:

//this will not longer work
val token = Authorization(OAuth2BearerToken(accessToken))
val pipeline: HttpRequest => Future[HttpResponse] = (addHeader(token) ~> sendReceive)
val patch: HttpRequest = Patch(uri, object))

pipeline(patch).map { response =>
    …
}

needs to be changed into:

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

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

Changes in form fields and file upload directives

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 =>
  ...
  }
}

Removed features

Removed HttpService

Spray’s HttpService was removed. This means that scala code like this:

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

needs to be changed into:

Http().newServerAt("0.0.0.0", port = 8080).bind(routes)

Other removed features

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.