Skip to content

Commit

Permalink
Added gzip filter that only gzip responses matching an allowed list o…
Browse files Browse the repository at this point in the history
…f content types
  • Loading branch information
jshiell authored and s4nchez committed Nov 2, 2018
1 parent 639d246 commit 6192820
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 17 deletions.
34 changes: 31 additions & 3 deletions http4k-core/src/main/kotlin/org/http4k/filter/ResponseFilters.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package org.http4k.filter

import org.http4k.core.Filter
import org.http4k.core.HttpTransaction
import org.http4k.core.Response
import org.http4k.core.*
import java.time.Clock
import java.time.Duration
import java.time.Duration.between
Expand Down Expand Up @@ -53,6 +51,36 @@ object ResponseFilters {
}
}

/**
* GZipping of the response where the content-type (sans-charset) matches an allowed list of compressible types.
*/
class GZipContentTypes(compressibleContentTypes: Set<ContentType>) : Filter {
private val compressibleMimeTypes = compressibleContentTypes
.map { it.value }
.map { it.split(";").first() }

override fun invoke(next: HttpHandler): HttpHandler {
return { request ->
next(request).let {
if (requestAcceptsGzip(request) && isCompressible(it)) {
it.body(it.body.gzipped()).replaceHeader("content-encoding", "gzip")
} else {
it
}
}
}
}

private fun isCompressible(it: Response) =
compressibleMimeTypes.contains(mimeTypeOf(it))

private fun mimeTypeOf(it: Response) =
(it.header("content-type") ?: "").split(";").first().trim()

private fun requestAcceptsGzip(it: Request) =
(it.header("accept-encoding") ?: "").contains("gzip", true)
}

/**
* Basic GZipping of Response. Does not currently support GZipping streams
*/
Expand Down
13 changes: 13 additions & 0 deletions http4k-core/src/main/kotlin/org/http4k/filter/ServerFilters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,19 @@ object ServerFilters {
operator fun invoke(): Filter = RequestFilters.GunZip().then(ResponseFilters.GZip())
}

/**
* Basic GZip and Gunzip support of Request/Response where the content-type is in the allowed list. Does not currently support GZipping streams.
* Only Gunzips requests which contain "transfer-encoding" header containing 'gzip'
* Only Gzips responses when request contains "accept-encoding" header containing 'gzip' and the content-type (sans-charset) is one of the compressible types.
*/
class GZipContentTypes(private val compressibleContentTypes: Set<ContentType>): Filter {
override fun invoke(next: HttpHandler): HttpHandler {
return RequestFilters.GunZip()
.then(ResponseFilters.GZipContentTypes(compressibleContentTypes))
.invoke(next)
}
}

/**
* Initialise a RequestContext for each request which passes through the Filter stack,
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ import com.natpryce.hamkrest.and
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.should.shouldMatch
import org.http4k.core.Body
import org.http4k.core.HttpTransaction
import org.http4k.core.*
import org.http4k.core.HttpTransaction.Companion.ROUTING_GROUP_LABEL
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.filter.ResponseFilters.ReportHttpTransaction
import org.http4k.hamkrest.hasBody
import org.http4k.hamkrest.hasHeader
Expand Down Expand Up @@ -64,6 +60,42 @@ class ResponseFiltersTest {
assertSupportsZipping("")
}

@Test
fun `gzip response and adds gzip content encoding if the request has accept-encoding of gzip and content type is acceptable`() {
fun assertSupportsZipping(body: String) {
val zipped = ResponseFilters.GZipContentTypes(setOf(ContentType.TEXT_HTML)).then { Response(OK).header("content-type", "text/html").body(body) }
zipped(Request(Method.GET, "").header("accept-encoding", "gzip")) shouldMatch
hasBody(equalTo(Body(body).gzipped())).and(hasHeader("content-encoding", "gzip"))
}
assertSupportsZipping("foobar")
assertSupportsZipping("")
}

@Test
fun `gzip response and adds gzip content encoding if the request has accept-encoding of gzip and content type with a charset is acceptable`() {
fun assertSupportsZipping(body: String) {
val zipped = ResponseFilters.GZipContentTypes(setOf(ContentType.TEXT_HTML)).then { Response(OK).header("content-type", "text/html;charset=utf-8").body(body) }
zipped(Request(Method.GET, "").header("accept-encoding", "gzip")) shouldMatch
hasBody(equalTo(Body(body).gzipped())).and(hasHeader("content-encoding", "gzip"))
}
assertSupportsZipping("foobar")
assertSupportsZipping("")
}

@Test
fun `do not gzip response if content type is missing`() {
val zipped = ResponseFilters.GZipContentTypes(setOf(ContentType.TEXT_HTML)).then { Response(OK).body("unzipped") }
zipped(Request(Method.GET, "").header("accept-encoding", "gzip")) shouldMatch
hasBody(equalTo(Body("unzipped"))).and(!hasHeader("content-encoding", "gzip"))
}

@Test
fun `do not gzip response if content type is not acceptable`() {
val zipped = ResponseFilters.GZipContentTypes(setOf(ContentType.TEXT_HTML)).then { Response(OK).header("content-type", "image/png").body("unzipped") }
zipped(Request(Method.GET, "").header("accept-encoding", "gzip")) shouldMatch
hasBody(equalTo(Body("unzipped"))).and(!hasHeader("content-encoding", "gzip"))
}

@Test
fun `gunzip response which has gzip content encoding`() {
fun assertSupportsUnzipping(body: String) {
Expand Down
43 changes: 34 additions & 9 deletions http4k-core/src/test/kotlin/org/http4k/filter/ServerFiltersTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,19 @@ import com.natpryce.hamkrest.equalTo
import com.natpryce.hamkrest.present
import com.natpryce.hamkrest.should.shouldMatch
import com.natpryce.hamkrest.throws
import org.http4k.core.Body
import org.http4k.core.*
import org.http4k.core.ContentType.Companion.OCTET_STREAM
import org.http4k.core.Filter
import org.http4k.core.Headers
import org.http4k.core.ContentType.Companion.TEXT_HTML
import org.http4k.core.Method.DELETE
import org.http4k.core.Method.GET
import org.http4k.core.Method.OPTIONS
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.RequestContext
import org.http4k.core.RequestContexts
import org.http4k.core.Response
import org.http4k.core.Status
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.I_M_A_TEAPOT
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.UNSUPPORTED_MEDIA_TYPE
import org.http4k.core.then
import org.http4k.filter.CorsPolicy.Companion.UnsafeGlobalPermissive
import org.http4k.filter.SamplingDecision.Companion.DO_NOT_SAMPLE
import org.http4k.filter.SamplingDecision.Companion.SAMPLE
Expand Down Expand Up @@ -183,6 +176,38 @@ class ServerFiltersTest {
handler(Request(GET, "/").body("hello"))
}

@Test
fun `gunzip request and gzip response with matching content type`() {
val handler = ServerFilters.GZipContentTypes(setOf(ContentType.TEXT_PLAIN)).then {
it shouldMatch hasBody(equalTo("hello"))
Response(OK).header("content-type", "text/plain").body(it.body)
}

handler(Request(Method.GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip").body(Body("hello").gzipped())) shouldMatch
hasHeader("content-encoding", "gzip").and(hasBody(equalTo(Body("hello").gzipped())))
}

@Test
fun `gunzip request and do not gzip response with unmatched content type`() {
val handler = ServerFilters.GZipContentTypes(setOf(ContentType.TEXT_HTML)).then {
it shouldMatch hasBody(equalTo("hello"))
Response(OK).header("content-type", "text/plain").body(it.body)
}

handler(Request(Method.GET, "/").header("accept-encoding", "gzip").header("content-encoding", "gzip").body(Body("hello").gzipped())) shouldMatch
!hasHeader("content-encoding", "gzip").and(hasBody(equalTo(Body("hello"))))
}

@Test
fun `passes through non-gzipped request despite content type`() {
val handler = ServerFilters.GZipContentTypes(setOf(TEXT_HTML)).then {
it shouldMatch hasBody("hello")
Response(OK).body("hello")
}

handler(Request(GET, "/").body("hello"))
}

@Test
fun `catch lens failure - custom response`() {
val e = LensFailure(Invalid(Header.required("bob").meta), Missing(Header.required("bill").meta))
Expand Down

0 comments on commit 6192820

Please sign in to comment.