!hco Make Content-Length and Content-Type visible but not constructible

This makes the `Content-Length` and `Content-Type` types visible - but not
constructible - outside of Akka HTTP. The change partially reverts #15800.
the http package. The types need to be visible so that Play can access
all parsed headers. However they types are not constructible outside Akka
HTTP because it is not desirable for users to create these headers. Users
should set the Content-Length and Content-Type via the members of the
HttpEntity objects.

Includes integration tests in the io.akka package to ensure that objects
have correct visibility outside akka.http.
This commit is contained in:
Rich Dougherty 2014-09-16 22:30:22 +12:00
parent a86c4528a7
commit 8588fb9fbf
2 changed files with 220 additions and 5 deletions

View file

@ -55,14 +55,12 @@ final case class Connection(tokens: immutable.Seq[String]) extends ModeledHeader
}
// http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-26#section-3.3.2
private[http] object `Content-Length` extends ModeledCompanion
object `Content-Length` extends ModeledCompanion
/**
* Instances of this class will only be created transiently during header parsing and will never appear
* in HttpMessage.header. To access the Content-Length, see subclasses of HttpEntity.
*
* INTERNAL API
*/
private[http] final case class `Content-Length`(length: Long)(implicit ev: ProtectedHeaderCreation.Enabled) extends ModeledHeader {
final case class `Content-Length` private[http] (length: Long)(implicit ev: ProtectedHeaderCreation.Enabled) extends ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ length
protected def companion = `Content-Length`
}
@ -349,7 +347,11 @@ final case class `Content-Range`(rangeUnit: RangeUnit, contentRange: ContentRang
// http://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-26#section-3.1.1.5
object `Content-Type` extends ModeledCompanion
final case class `Content-Type`(contentType: ContentType) extends japi.headers.ContentType with ModeledHeader {
/**
* Instances of this class will only be created transiently during header parsing and will never appear
* in HttpMessage.header. To access the Content-Type, see subclasses of HttpEntity.
*/
final case class `Content-Type` private[http] (contentType: ContentType) extends japi.headers.ContentType with ModeledHeader {
def renderValue[R <: Rendering](r: R): r.type = r ~~ contentType
protected def companion = `Content-Type`

View file

@ -0,0 +1,213 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package io.akka.integrationtest.http
import akka.actor.ActorSystem
import akka.http.model._
import akka.http.model.ContentTypes
import akka.http.model.headers._
import akka.http.model.parser.HeaderParser
import akka.stream.scaladsl2._
import akka.util.ByteString
import com.typesafe.config.{ ConfigFactory, Config }
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec }
import scala.concurrent.duration._
/**
* Integration test for external HTTP libraries that are built on top of
* Akka HTTP core. An example of an external library is Play Framework.
* The way these libraries use Akka HTTP core may be different to how
* normal users would use Akka HTTP core. For example the libraries may
* need direct access to some model objects that users who use Akka HTTP
* directly would not require.
*
* This test is designed to capture the needs of these external libaries.
* Each test gives a use case of an external library and then checks that
* it can be fulfilled. A typical example of a use case is converting
* between one library's HTTP model and the Akka HTTP core HTTP model.
*
* This test is located a different package (io.akka vs akka) in order to
* check for any visibility issues when Akka HTTP core is used by third
* party libraries.
*/
class HttpModelIntegrationSpec extends WordSpec with Matchers with BeforeAndAfterAll {
val testConf: Config = ConfigFactory.parseString("""
akka.event-handlers = ["akka.testkit.TestEventListener"]
akka.loglevel = WARNING""")
implicit val system = ActorSystem(getClass.getSimpleName, testConf)
import system.dispatcher
override def afterAll() = system.shutdown()
implicit val materializer = FlowMaterializer()
implicit val materializer1 = akka.stream.FlowMaterializer() // Needed by HttpEntity.toStrict
"External HTTP libraries" should {
"be able to get String headers and an Array[Byte] body out of an HttpRequest" in {
// First create an incoming HttpRequest for the external HTTP library
// to deal with. We're going to convert this request into the library's
// own HTTP model.
val request = HttpRequest(
method = HttpMethods.POST,
uri = Uri("/greeting"),
headers = List(
Host("localhost"),
RawHeader("Origin", "null")),
entity = HttpEntity.Default(
contentType = ContentTypes.`application/json`,
contentLength = 5,
FlowFrom(List(ByteString("hello"))).toPublisher()))
// Our library uses a simple model of headers: a Seq[(String, String)].
// The body is represented as an Array[Byte]. To get the headers in
// the form we want we first need to reconstruct the original headers
// by combining the request.headers value with the headers that are
// implicitly contained by the request.entity. We convert all the
// HttpHeaders by getting their name and value. We convert Content-Type
// and Content-Length by using the toString of their values.
val partialTextHeaders: Seq[(String, String)] = request.headers.map(h (h.name, h.value))
val entityTextHeaders: Seq[(String, String)] = request.entity match {
case HttpEntity.Default(contentType, contentLength, _)
Seq(("Content-Type", contentType.toString), ("Content-Length", contentLength.toString))
case _
???
}
val textHeaders: Seq[(String, String)] = entityTextHeaders ++ partialTextHeaders
textHeaders shouldEqual Seq(
"Content-Type" -> "application/json; charset=UTF-8",
"Content-Length" -> "5",
"Host" -> "localhost",
"Origin" -> "null")
// Finally convert the body into an Array[Byte].
val entityBytes: Array[Byte] = request.entity.toStrict(1.second).await(2.seconds).data.toArray
entityBytes.to[Seq] shouldEqual ByteString("hello").to[Seq]
}
"be able to build an HttpResponse from String headers and Array[Byte] body" in {
// External HTTP libraries (such as Play) will model HTTP differently
// to Akka HTTP. One model uses a Seq[(String, String)] for headers and
// an Array[Byte] for a body. The following data structures show an
// example simple model of an HTTP response.
val textHeaders: Seq[(String, String)] = Seq(
"Content-Type" -> "text/plain",
"Content-Length" -> "3",
"X-Greeting" -> "Hello")
val byteArrayBody: Array[Byte] = "foo".getBytes
// Now we need to convert this model to Akka HTTP's model. To do that
// we use Akka HTTP's HeaderParser to parse the headers, giving us a
// List[HttpHeader].
val rawHeaders = textHeaders.map { case (name, value) RawHeader(name, value) }
val (parseErrors, convertedHeaders): (List[_], List[HttpHeader]) = HeaderParser.parseHeaders(rawHeaders.to[List])
parseErrors shouldEqual Nil
// Most of these headers are modeled by Akka HTTP as a Seq[HttpHeader],
// but the the Content-Type and Content-Length are special: their
// values relate to the HttpEntity and so they're modeled as part of
// the HttpEntity. These headers need to be stripped out of the main
// Seq[Header] and dealt with separately.
val normalHeaders = convertedHeaders.filter {
case _: `Content-Type` false
case _: `Content-Length` false
case _ true
}
normalHeaders.head shouldEqual RawHeader("X-Greeting", "Hello")
normalHeaders.tail shouldEqual Nil
val contentType = convertedHeaders.collectFirst {
case ct: `Content-Type` ct.contentType
}
contentType shouldEqual Some(ContentTypes.`text/plain`)
val contentLength = convertedHeaders.collectFirst {
case cl: `Content-Length` cl.length
}
contentLength shouldEqual Some(3)
// We're going to model the HttpEntity as an HttpEntity.Default, so
// convert the body into a Publisher[ByteString].
val byteStringBody = ByteString(byteArrayBody)
val publisherBody = FlowFrom(List(byteStringBody)).toPublisher()
// Finally we can create our HttpResponse.
HttpResponse(
entity = HttpEntity.Default(contentType.get, contentLength.get, publisherBody))
}
"be able to wrap HttpHeaders with custom typed headers" in {
// This HTTP model is typed. It uses Akka HTTP types internally, but
// no Akka HTTP types are visible to users. This typed model is a
// model that Play Framework may eventually move to.
object ExampleLibrary {
trait TypedHeader {
def name: String
def value: String
}
private[ExampleLibrary] case class GenericHeader(internal: HttpHeader) extends TypedHeader {
def name: String = internal.name
def value: String = internal.value
}
private[ExampleLibrary] case class ContentTypeHeader(contentType: ContentType) extends TypedHeader {
def name: String = "Content-Type"
def value: String = contentType.toString
}
private[ExampleLibrary] case class ContentLengthHeader(length: Long) extends TypedHeader {
def name: String = "Content-Length"
def value: String = length.toString
}
// Headers can be created from strings.
def header(name: String, value: String): TypedHeader = {
val parsedHeader = HeaderParser.parseHeader(RawHeader(name, value)).fold(
error sys.error(s"Failed to parse: $error"),
parsed parsed)
parsedHeader match {
case `Content-Type`(contentType) ContentTypeHeader(contentType)
case `Content-Length`(length) ContentLengthHeader(length)
case _ GenericHeader(parsedHeader)
}
}
// Or they can be created more directly, without parsing.
def contentType(contentType: ContentType): TypedHeader =
new ContentTypeHeader(contentType)
def contentLength(length: Long): TypedHeader =
new ContentLengthHeader(length)
}
// Users of ExampleLibrary should be able to create headers by
// parsing strings
ExampleLibrary.header("X-Y-Z", "abc")
ExampleLibrary.header("Host", "null")
ExampleLibrary.header("Content-Length", "3")
// Users should also be able to create headers directly. Internally
// we want avoid parsing when possible (for efficiency), so it's good
// to be able to directly create a ContentType and ContentLength
// headers.
ExampleLibrary.contentLength(3)
ExampleLibrary.contentType(ContentTypes.`text/plain`)
}
}
}