7

I'm working with a legacy HTTP API (that I can't change) that responds with JSON in the body, but gives a Content-Type: text/plain; charset=utf-8 header.

I am attempting to unmarshall that HTTP body as JSON, but I get the following exception: akka.http.scaladsl.unmarshalling.Unmarshaller$UnsupportedContentTypeException: Unsupported Content-Type, supported: application/json

My code looks like this:

import spray.json.DefaultJsonProtocol
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.unmarshalling._

case class ResponseBody(status: String, error_msg: String)

object ResponseBodyJsonProtocol extends DefaultJsonProtocol {
  implicit val responseBodyFormat = jsonFormat2(ResponseBody)
}

def parse(entity: HttpEntity): Future[ResponseBody] = {
  implicit val materializer: Materializer = ActorMaterializer()
  import ResponseBodyJsonProtocol._
  Unmarshal[HttpEntity](entity).to[ResponseBody]
}

A sample HTTP response looks like this:

HTTP/1.1 200 OK
Cache-Control: private
Content-Encoding: gzip
Content-Length: 161
Content-Type: text/plain; charset=utf-8
Date: Wed, 16 Dec 2015 18:15:14 GMT
Server: Microsoft-IIS/7.5
Vary: Accept-Encoding
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET

{"status":"1","error_msg":"Missing parameter"}

What can I do to ignore the Content-Type in the HTTP response and parse as JSON?

David van Geest
  • 1,957
  • 1
  • 20
  • 19

3 Answers3

9

One workaround I found was to just manually set the Content-Type on the HttpEntity before unmarshalling it:

def parse(entity: HttpEntity): Future[ResponseBody] = {
  implicit val materializer: Materializer = ActorMaterializer()
  import ResponseBodyJsonProtocol._
  Unmarshal[HttpEntity](entity.withContentType(ContentTypes.`application/json`)).to[ResponseBody]
}

Seems to work OK, but I'm open to other ideas...

David van Geest
  • 1,957
  • 1
  • 20
  • 19
  • I had a similar problem and used your last line of code for Unmarshalling. For some reason the withContentType alters the entity and transforms it to a strict entity which contains also the content type object. Any idea on how to extract the body from that? – Lucian Enache Feb 01 '16 at 14:02
2

I'd use map... directive. It looks short and elegant.

val routes = (decodeRequest & encodeResponse) {
  mapResponseEntity(_.withContentType(ContentTypes.`application/json`)) {
    nakedRoutes ~ authenticatedRoutes
  }
}
expert
  • 29,290
  • 30
  • 110
  • 214
  • I'm not sure this applies.... there's no routing going on in my case, and I certainly don't see how this is shorter or more elegant than my answer. Same idea though. – David van Geest Dec 16 '15 at 21:58
  • 1
    Oh yes, if you don't use Spray DSL then it doesn't apply. – expert Dec 17 '15 at 01:31
0

An alternative solution: create a custom unmarshaller that accepts any content type (or any list of content types):

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsValueByteStringUnmarshaller
import akka.http.scaladsl.model.ContentTypeRange
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
import spray.json.{RootJsonReader, jsonReader}

// Add this to scope, INSTEAD OF importing SprayJsonSupport._
// It returns an Unmarshaller identical to SprayJsonSupport.sprayJsonUnmarshaller, but with a custom validation on content type
implicit def lenientJsonUnmarshaller[T](implicit reader: RootJsonReader[T]): FromEntityUnmarshaller[T] =
  Unmarshaller.byteStringUnmarshaller
    .forContentTypes(ContentTypeRange.*) // or any range you'd like
    .andThen(sprayJsValueByteStringUnmarshaller)
    .map(jsonReader[T].read)

Then - with that in scope, continue as you did:

def parse(entity: HttpEntity): Future[ResponseBody] = {
  implicit val materializer: Materializer = ActorMaterializer()
  import ResponseBodyJsonProtocol._
  Unmarshal[HttpEntity](entity).to[ResponseBody] // this implicitly uses the custom unmarshaller
}

The benefit - it's easy to reuse this implicit unmarshaller - write it once and just import it wherever you need, instead of having to set the content-type on the entity.

Tzach Zohar
  • 37,442
  • 3
  • 79
  • 85