akka / akka-http

The Streaming-first HTTP server/module of Akka
https://doc.akka.io/docs/akka-http
Other
1.34k stars 596 forks source link

Trouble with "Resource representation is only available with these Content-Types" #121

Open ktoso opened 8 years ago

ktoso commented 8 years ago

Issue by matthewadams Tuesday Sep 08, 2015 at 15:29 GMT Originally opened as https://github.com/akka/akka/issues/18425


NB: discussion at https://groups.google.com/forum/#!topic/akka-user/kpdsQsQVZJQ

I'm trying to allow a client to control (via the Accept header) the MediaType used when a site is responding, provided that the Accept header contains one of a collection of supported MediaTypes. For example, I've written an endpoint to support application/json, text/xml, text/html and text/plain, and I'd like the client to be able to express its preference for which format it would like via the Accept header, such that Accept: text/xml would cause the endpoint to use Content-Type: text/xml in the response and send the response as xml. Same deal with application/json, text/html & text/plain.

I saw the discussion at https://groups.google.com/forum/#!topic/akka-user/_054Z0G622w/discussion (where respondWithMediaType from Spray is considered an antipattern), so I wrote some code that I thought would work:

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.headers.Accept
import akka.stream.ActorMaterializer
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model._
import akka.http.scaladsl.marshalling._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import spray.json.DefaultJsonProtocol._
import spray.json.DefaultJsonProtocol
import akka.http.scaladsl.server.{Rejection, Directive1, Directive}

object Main extends App {

  // start infrastructure
  implicit val system = ActorSystem("akka-http-routing")
  implicit val materializer = ActorMaterializer()

  val json = MediaTypes.`application/json`
  val xml = MediaTypes.`text/xml`
  val html = MediaTypes.`text/html`
  val text = MediaTypes.`text/plain`

  case class MissingAcceptHeaderMediaType(requiredMediaType: Seq[MediaType]) extends Rejection

  case class Pong(s: String = "")
  implicit val pjf = jsonFormat1(Pong)

  def firstIn[In, Among](in: Seq[In], among: Seq[Among], predicate: (In, Among) => Boolean): Option[In] =
    in collectFirst { case i if among exists { a => predicate(i, a) } => i }

  def firstAmong[In, Among](in: Seq[In], among: Seq[Among], predicate: (In, Among) => Boolean): Option[Among] =
    firstIn(in, among, predicate) match {
      case Some(i) => among collectFirst { case a if predicate(i, a) => a }
      case _ => None
    }

  def firstMediaType(in: Accept, among: Seq[MediaType]): Option[MediaType] = {
    firstAmong(in.mediaRanges, among, (r: MediaRange, t: MediaType) => r matches t)
  }

  def accept(requiredTypes: Seq[MediaType]): Directive1[MediaType] = headerValueByType[Accept]().flatMap {
    case v => firstMediaType(v, requiredTypes) match {
      case Some(mt) => extract { f => mt }
    }
    case _ => reject(MissingAcceptHeaderMediaType(requiredTypes))
  }

  val route =
    get {
      path("ping") {
        accept(List(json, xml, html, text)) { t =>
          val s = s"""MediaType is ${t}"""
          t match {
            case json => complete {
              Pong(s)
            }
            case xml => complete {
              <pong>
                {s}
              </pong>
            }
            case html => complete {
              <html>
                <body>
                  <pre>
                    {s}
                  </pre>
                </body>
              </html>
            }
            case _ => complete {
              Pong(s).toString
            }
          }
        }
      }
    }

  // start a new HTTP server on port 8080 with our route
  Http().bindAndHandle(route, "localhost", 8080)
}

When requesting the endpoint with an Accept header with value text/xml, I get the following error:

$ curl -i -H 'Accept: text/xml' http://localhost:8080/ping
HTTP/1.1 406 Not Acceptable
Server: akka-http/2.3.12
Date: Tue, 08 Sep 2015 15:13:47 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 84

Resource representation is only available with these Content-Types:
application/json

In an effort to reproduce the error with less code, I created the following class Bug (based on the above class Main):

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.headers.Accept
import akka.stream.ActorMaterializer
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.model._
import akka.http.scaladsl.marshalling._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import spray.json.DefaultJsonProtocol._
import spray.json.DefaultJsonProtocol
import akka.http.scaladsl.server.{Rejection, Directive1, Directive}

object Bug extends App {

  // start infrastructure
  implicit val system = ActorSystem("akka-http-routing")
  implicit val materializer = ActorMaterializer()

  case class Pong(s: String = "")
  implicit val pjf = jsonFormat1(Pong)

  val route =
    get {
      path("ping") {
        headerValueByName("X-REPRO-BUG") { t =>
          val s = s"""MediaType is ${t}"""
          t match {
            case "json" => complete {
              Pong(s)
            }
            case "xml" => complete {
<pong>
  {s}
</pong>
            }
            case "html" => complete {
<html>
  <body>
    <pre>
      {s}
    </pre>
  </body>
</html>
            }
            case _ => complete {
              Pong(s).toString
            }
          }
        }
      }
    }

  // start a new HTTP server on port 8080 with our route
  Http().bindAndHandle(route, "localhost", 8080)
}

Interestingly, in the presence of my custom X-REPRO-BUG header and the absence of an Accept header, the Bug class works as expected. If, however, I add an Accept header, I get an error response:

$ curl -i -H 'X-REPRO-BUG: xml' -H 'Accept: application/json' http://localhost:8080/ping
HTTP/1.1 406 Not Acceptable
Server: akka-http/2.3.12
Date: Tue, 08 Sep 2015 15:21:47 GMT
Content-Type: text/plain; charset=UTF-8
Content-Length: 124

Resource representation is only available with these Content-Types:
text/xml
application/xml
text/html
application/xhtml+xml
ktoso commented 8 years ago

Comment by 2beaucoup Wednesday Sep 09, 2015 at 07:45 GMT


You basically reimplemented content negotiation here, which akka-http already provides.

I think Marshaller.oneOf is what you are looking for. Just bring a marshaller defined with that implicitly in scope and use the marshalling directives.