Merge pull request #15895 from richdougherty/richd-http-integration-2.3

+hco Make `Content-Length` header public again and add integration tests
This commit is contained in:
Björn Antonsson 2014-09-22 11:16:06 +02:00
commit 2bdfea25c7
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`)
}
}
}