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:
commit
2bdfea25c7
2 changed files with 220 additions and 5 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue