Skip to content
This repository has been archived by the owner on Apr 24, 2024. It is now read-only.

Commit

Permalink
! routing: have encodeResponse automatically tie in `autoChunkFileB…
Browse files Browse the repository at this point in the history
…ytes`

 In order to prevent `encodeResponse` from having to load the complete file content for compression it needs to automatically apply `autoChunkFileBytes` to its inner route. However, since the latter requires an implicit ActorRefFactory (for being able to spawn the chunk streaming actor underneath) we need to refactor `encodeResponse` to a magnet based signature.
 This change trickles down to all depending directives, most importantly `compressResponse`. The "no-paren overload" of `compressResponse` needs to go (since it cannot be magnetized), which opens up the chance to unify `compressResponse` and `compresseResponseWith`.
 For symmetry we also change `decompressRequest` accordingly.
  • Loading branch information
sirthias committed Sep 10, 2013
1 parent fcc83fc commit e3defb4
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 53 deletions.
Expand Up @@ -71,7 +71,7 @@ class HttpServiceExamplesSpec extends Specification with Specs2RouteTest {
} ~
post {
// decompresses the request with Gzip or Deflate when required
decompressRequest {
decompressRequest() {
// unmarshal with in-scope unmarshaller
entity(as[Order]) { order =>
// transfer to newly spawned actor
Expand Down Expand Up @@ -126,7 +126,7 @@ class HttpServiceExamplesSpec extends Specification with Specs2RouteTest {
cache(simpleCache) {
// optionally compresses the response with Gzip or Deflate
// if the client accepts compressed responses
compressResponse {
compressResponse() {
// serve up static content from a JAR resource
getFromResourceDirectory("docs")
}
Expand Down
Expand Up @@ -262,31 +262,31 @@ class EncodingDirectivesSpec extends RoutingSpec {
"the compressResponse directive" should {
"produce a GZIP compressed response if the request has no Accept-Encoding header" in {
Get("/") ~> {
compressResponse { yeah }
compressResponse() { yeah }
} ~> check {
response must haveContentEncoding(gzip)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a GZIP compressed response if the request has an `Accept-Encoding: gzip, deflate` header" in {
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> {
compressResponse { yeah }
compressResponse() { yeah }
} ~> check {
response must haveContentEncoding(gzip)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a Deflate compressed response if the request has an `Accept-Encoding: deflate` header" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
compressResponse { yeah }
compressResponse() { yeah }
} ~> check {
response must haveContentEncoding(deflate)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
}
}
"produce an uncompressed response if the request has an `Accept-Encoding: identity` header" in {
Get("/") ~> `Accept-Encoding`(identity) ~> {
compressResponse { completeOk }
compressResponse() { completeOk }
} ~> check {
response === Ok
response must haveNoContentEncoding
Expand All @@ -299,31 +299,31 @@ class EncodingDirectivesSpec extends RoutingSpec {
"the compressResponseIfRequested directive" should {
"produce an uncompressed response if the request has no Accept-Encoding header" in {
Get("/") ~> {
compressResponseIfRequested { yeah }
compressResponseIfRequested() { yeah }
} ~> check {
response must haveNoContentEncoding
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), "Yeah!")
}
}
"produce a GZIP compressed response if the request has an `Accept-Encoding: deflate, gzip` header" in {
Get("/") ~> `Accept-Encoding`(deflate, gzip) ~> {
compressResponseIfRequested { yeah }
compressResponseIfRequested() { yeah }
} ~> check {
response must haveContentEncoding(gzip)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
compressResponseIfRequested { yeah }
compressResponseIfRequested() { yeah }
} ~> check {
response must haveContentEncoding(deflate)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
}
}
"produce an uncompressed response if the request has an `Accept-Encoding: identity` header" in {
Get("/") ~> `Accept-Encoding`(identity) ~> {
compressResponseIfRequested { completeOk }
compressResponseIfRequested() { completeOk }
} ~> check {
response === Ok
response must haveNoContentEncoding
Expand All @@ -336,31 +336,31 @@ class EncodingDirectivesSpec extends RoutingSpec {
"the compressResponseWith directive" should {
"produce a response compressed with the specified Encoder if the request has a matching Accept-Encoding header" in {
Get("/") ~> `Accept-Encoding`(gzip) ~> {
compressResponseWith(Gzip) { yeah }
compressResponse(Gzip) { yeah }
} ~> check {
response must haveContentEncoding(gzip)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"produce a response compressed with one of the specified Encoders if the request has a matching Accept-Encoding header" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
compressResponseWith(Gzip, Deflate) { yeah }
compressResponse(Gzip, Deflate) { yeah }
} ~> check {
response must haveContentEncoding(deflate)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
}
}
"produce a response compressed with the first of the specified Encoders if the request has no Accept-Encoding header" in {
Get("/") ~> {
compressResponseWith(Gzip, Deflate) { yeah }
compressResponse(Gzip, Deflate) { yeah }
} ~> check {
response must haveContentEncoding(gzip)
body === HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
}
}
"reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> {
compressResponseWith(Gzip) { yeah }
compressResponse(Gzip) { yeah }
} ~> check {
rejection === UnacceptedResponseEncodingRejection(gzip)
}
Expand All @@ -372,31 +372,29 @@ class EncodingDirectivesSpec extends RoutingSpec {
"the decompressRequest directive" should {
"decompress the request content if it has a `Content-Encoding: gzip` header and the content is gzip encoded" in {
Get("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decompressRequest { echoRequestContent }
decompressRequest() { echoRequestContent }
} ~> check { entityAs[String] === "Hello" }
}
"decompress the request content if it has a `Content-Encoding: deflate` header and the content is deflate encoded" in {
Get("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> {
decompressRequest { echoRequestContent }
decompressRequest() { echoRequestContent }
} ~> check { entityAs[String] === "Hello" }
}
"decompress the request content if it has a `Content-Encoding: identity` header and the content is not encoded" in {
Get("/", "yes") ~> `Content-Encoding`(identity) ~> {
decompressRequest { echoRequestContent }
decompressRequest() { echoRequestContent }
} ~> check { entityAs[String] === "yes" }
}
"decompress the request content using NoEncoding if no Content-Encoding header is present" in {
Get("/", "yes") ~> decompressRequest { echoRequestContent } ~> check { entityAs[String] === "yes" }
Get("/", "yes") ~> decompressRequest() { echoRequestContent } ~> check { entityAs[String] === "yes" }
}
"reject the request if it has a `Content-Encoding: deflate` header but the request is compressed with Gzip" in {
Get("/", helloGzipped) ~> `Content-Encoding`(deflate) ~> {
decompressRequest { echoRequestContent }
decompressRequest() { echoRequestContent }
} ~> check {
rejections must beLike {
case Seq(UnsupportedRequestEncodingRejection(`gzip`),
CorruptRequestEncodingRejection(_),
UnsupportedRequestEncodingRejection(`identity`)) ok
}
rejections(0) === UnsupportedRequestEncodingRejection(gzip)
rejections(1) must beAnInstanceOf[CorruptRequestEncodingRejection]
rejections(2) === UnsupportedRequestEncodingRejection(identity)
}
}
}
Expand All @@ -406,18 +404,18 @@ class EncodingDirectivesSpec extends RoutingSpec {
"the decompressRequestWith directive" should {
"decompress the request content if its `Content-Encoding` header matches the specified encoder" in {
Get("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decompressRequestWith(Gzip) { echoRequestContent }
decompressRequest(Gzip) { echoRequestContent }
} ~> check { entityAs[String] === "Hello" }
}
"reject the request if its `Content-Encoding` header doesn't match the specified encoder" in {
Get("/", helloGzipped) ~> `Content-Encoding`(deflate) ~> {
decompressRequestWith(Gzip) { echoRequestContent }
decompressRequest(Gzip) { echoRequestContent }
} ~> check {
rejection === UnsupportedRequestEncodingRejection(gzip)
}
}
"reject the request when decompressing with GZIP and no Content-Encoding header is present" in {
Get("/", "yes") ~> decompressRequestWith(Gzip) { echoRequestContent } ~> check {
Get("/", "yes") ~> decompressRequest(Gzip) { echoRequestContent } ~> check {
rejection === UnsupportedRequestEncodingRejection(gzip)
}
}
Expand All @@ -426,7 +424,7 @@ class EncodingDirectivesSpec extends RoutingSpec {

//# decompress-compress-combination-example
"the (decompressRequest & compressResponse) compound directive" should {
val decompressCompress = (decompressRequest & compressResponse)
val decompressCompress = (decompressRequest() & compressResponse())
"decompress a GZIP compressed request and produce a GZIP compressed response if the request has no Accept-Encoding header" in {
Get("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decompressCompress { echoRequestContent }
Expand Down Expand Up @@ -455,7 +453,7 @@ class EncodingDirectivesSpec extends RoutingSpec {
//#

"the (decompressRequest & compressResponseIfRequested) compound directive" should {
val decompressCompressIfRequested = (decompressRequest & compressResponseIfRequested)
val decompressCompressIfRequested = (decompressRequest() & compressResponseIfRequested())
"decode a GZIP encoded request and produce a non-encoded response if the request has no Accept-Encoding header" in {
Get("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
decompressCompressIfRequested { echoRequestContent }
Expand Down
Expand Up @@ -20,9 +20,11 @@ package directives
import spray.util._
import spray.http._
import spray.httpx.encoding._
import akka.actor.ActorRefFactory

trait EncodingDirectives {
import BasicDirectives._
import ChunkingDirectives._
import MiscDirectives._
import RouteDirectives._

Expand Down Expand Up @@ -55,7 +57,8 @@ trait EncodingDirectives {
/**
* Wraps its inner Route with encoding support using the given Encoder.
*/
def encodeResponse(encoder: Encoder) = {
def encodeResponse(magnet: EncodeResponseMagnet) = {
import magnet._
def applyEncoder = mapRequestContext { ctx
@volatile var compressor: Compressor = null
ctx.withHttpResponsePartMultiplied {
Expand All @@ -76,7 +79,8 @@ trait EncodingDirectives {
}
responseEncodingAccepted(encoder.encoding) &
applyEncoder &
cancelAllRejections(ofType[UnacceptedResponseEncodingRejection])
cancelAllRejections(ofType[UnacceptedResponseEncodingRejection]) &
autoChunkFileBytes(autoChunkThreshold, autoChunkSize)
}

/**
Expand All @@ -88,41 +92,65 @@ trait EncodingDirectives {
.flatMap(if (_) pass else reject(UnacceptedResponseEncodingRejection(encoding)))

/**
* Wraps its inner Route with response compression, only falling back to
* uncompressed responses if the client specifically requests the "identity"
* encoding and preferring Gzip over Deflate.
* Wraps its inner Route with response compression, using the specified
* encoders in the given order of preference.
* If no encoders are specifically given Gzip, Deflate and NoEncoding
* are used in this order, depending on what the client accepts.
*/
def compressResponse: Directive0 = compressResponseWith(Gzip, Deflate, NoEncoding)
def compressResponse(magnet: CompressResponseMagnet): Directive0 = {
import magnet._
encoders.tail.foldLeft(encodeResponse(encoders.head)) { (r, encoder) r | encodeResponse(encoder) }
}

/**
* Wraps its inner Route with response compression if and only if the client
* specifically requests compression with an Accept-Encoding header.
*/
def compressResponseIfRequested: Directive0 = compressResponseWith(NoEncoding, Gzip, Deflate)

/**
* Wraps its inner Route with response compression, using the specified
* encoders in order of preference.
* specifically requests compression with an `Accept-Encoding` header.
*/
def compressResponseWith(first: Encoder, more: Encoder*): Directive0 =
if (more.isEmpty) encodeResponse(first)
else more.foldLeft(encodeResponse(first)) { (r, encoder) r | encodeResponse(encoder) }
def compressResponseIfRequested(magnet: CompressResponseMagnet): Directive0 = {
import magnet._
compressResponse(NoEncoding, Gzip, Deflate)
}

/**
* Wraps its inner Route with request decompression, assuming
* Gzip compressed requests but falling back to Deflate or no
* compression if the request contains the relevant Content-Encoding
* header.
* Decompresses the incoming request if it is GZip or Deflate encoded.
* Uncompressed requests are passed on to the inner route unchanged.
*/
def decompressRequest: Directive0 = decompressRequestWith(Gzip, Deflate, NoEncoding)
def decompressRequest(): Directive0 = decompressRequest(Gzip, Deflate, NoEncoding)

/**
* Wraps its inner Route with request decompression, trying the specified
* decoders in turn.
* Decompresses the incoming request if it encoded with one of the given
* encoders. If the request encoding doesn't match one of the given encoders
* the request is rejected with an `UnsupportedRequestEncodingRejection`.
*/
def decompressRequestWith(first: Decoder, more: Decoder*): Directive0 =
def decompressRequest(first: Decoder, more: Decoder*): Directive0 =
if (more.isEmpty) decodeRequest(first)
else more.foldLeft(decodeRequest(first)) { (r, decoder) r | decodeRequest(decoder) }
}

object EncodingDirectives extends EncodingDirectives

class EncodeResponseMagnet(val encoder: Encoder, val autoChunkThreshold: Long = 128 * 1024,
val autoChunkSize: Int = 128 * 1024)(implicit val refFactory: ActorRefFactory)
object EncodeResponseMagnet {
implicit def fromEncoder(encoder: Encoder)(implicit factory: ActorRefFactory): EncodeResponseMagnet =
new EncodeResponseMagnet(encoder)
implicit def fromEncoderThresholdAndChunkSize(t: (Encoder, Long, Int))(implicit factory: ActorRefFactory): EncodeResponseMagnet =
new EncodeResponseMagnet(t._1, t._2, t._3)
}

class CompressResponseMagnet(val encoders: List[Encoder])(implicit val refFactory: ActorRefFactory)
object CompressResponseMagnet {
implicit def fromUnit(u: Unit)(implicit refFactory: ActorRefFactory): CompressResponseMagnet =
new CompressResponseMagnet(Gzip :: Deflate :: NoEncoding :: Nil)
implicit def fromEncoders1(e: Encoder)(implicit refFactory: ActorRefFactory): CompressResponseMagnet =
new CompressResponseMagnet(e :: Nil)
implicit def fromEncoders2(t: (Encoder, Encoder))(implicit refFactory: ActorRefFactory): CompressResponseMagnet =
new CompressResponseMagnet(t._1 :: t._2 :: Nil)
implicit def fromEncoders3(t: (Encoder, Encoder, Encoder))(implicit refFactory: ActorRefFactory): CompressResponseMagnet =
new CompressResponseMagnet(t._1 :: t._2 :: t._3 :: Nil)
}

class RefFactoryMagnet(implicit val refFactory: ActorRefFactory)
object RefFactoryMagnet {
implicit def fromUnit(u: Unit)(implicit refFactory: ActorRefFactory): RefFactoryMagnet = new RefFactoryMagnet
}

0 comments on commit e3defb4

Please sign in to comment.