!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:
parent
a86c4528a7
commit
8588fb9fbf
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