Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for compression of responses #563

Merged
merged 3 commits into from
Dec 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ final class ServerConfigSchema {
STYX_SERVER_CONFIGURATION_SCHEMA_BUILDER = newDocument()
.rootSchema(object(
field("proxy", object(
optional("compressResponses", bool()),
field("connectors", serverConnectorsSchema),
optional("bossThreadsCount", integer()),
optional("clientWorkerThreadsCount", integer()),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.hotels.styx.proxy;

import io.netty.handler.codec.http.HttpContentCompressor;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponse;

import java.util.Arrays;
import java.util.Collection;

/**
* Compress HTTP responses if the encoding type is compressable.
* List of compressable encoding types:
* "text/plain",
* "text/html",
* "text/xml",
* "text/css",
* "text/json",
* "application/xml",
* "application/xhtml+xml",
* "application/rss+xml",
* "application/javascript",
* "application/x-javascript",
* "application/json"
*/
public class HttpCompressor extends HttpContentCompressor {

private static final Collection<String> ENCODING_TYPES = Arrays.asList(
"text/plain",
"text/html",
"text/xml",
"text/css",
"text/json",
"application/xml",
"application/xhtml+xml",
"application/rss+xml",
"application/javascript",
"application/x-javascript",
"application/json");


private boolean shouldCompress(String contentType) {
return ENCODING_TYPES.contains(contentType != null ? contentType.toLowerCase() : "");
}

@Override
protected Result beginEncode(HttpResponse response, String acceptEncoding) throws Exception {
String contentType = response.headers().get(HttpHeaderNames.CONTENT_TYPE);

if (shouldCompress(contentType)) {
return super.beginEncode(response, acceptEncoding);
} else {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ public void configure(Channel channel, HttpHandler httpPipeline) {
.secure(sslContext.isPresent())
.requestTracker(requestTracker)
.build());

if (serverConfig.compressResponses()) {
channel.pipeline().addBefore("styx-decoder", "compression", new HttpCompressor());
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ public Builder setClientWorkerThreadsCount(Integer clientWorkerThreadsCount) {
return this;
}

@JsonProperty("compressResponses")
public Builder setCompressResponses(boolean compressResponses) {
builder.setCompressResponses(compressResponses);
return this;
}

public ProxyServerConfig build() {
if (clientWorkerThreadsCount == null || clientWorkerThreadsCount == 0) {
clientWorkerThreadsCount = HALF_OF_AVAILABLE_PROCESSORS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class NettyServerConfigTest {
public void shouldCreateServerConfiguration() {
String yaml = "" +
"proxy:\n" +
" compressResponses: true\n" +
" connectors:\n" +
" http:\n" +
" port: 8080\n" +
Expand All @@ -48,6 +49,7 @@ public void shouldCreateServerConfiguration() {
.certificateKeyFile("example")
.build();
assertThat(httpsConnectorConfig(serverConfig), is(httpsConfig));
assertThat(serverConfig.compressResponses(), is(true));
}

private HttpsConnectorConfig httpsConnectorConfig(NettyServerConfig serverConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class ServerConfigSchemaTest : DescribeSpec({
it("Validates a minimal server configuration") {
validateServerConfiguration(yamlConfig("""
proxy:
compressResponses: true
connectors:
http:
port: 8080
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class NettyServerConfig {
private int requestTimeoutMs = 12000;
private int keepAliveTimeoutMillis = 12000;
private int maxConnectionsCount = 512;
private boolean compressResponses;

private final Optional<HttpConnectorConfig> httpConnectorConfig;
private final Optional<HttpsConnectorConfig> httpsConnectorConfig;
Expand Down Expand Up @@ -78,7 +79,7 @@ protected NettyServerConfig(Builder<?> builder) {

this.httpConnectorConfig = Optional.ofNullable(builder.httpConnectorConfig);
this.httpsConnectorConfig = Optional.ofNullable(builder.httpsConnectorConfig);

this.compressResponses = builder.compressResponses;
this.connectors = connectorsIterable();
}

Expand Down Expand Up @@ -179,6 +180,15 @@ public int maxConnectionsCount() {
return this.maxConnectionsCount;
}

/**
* Whether responses should be compressed.
*
* @return true if response compression is enabled
*/
public boolean compressResponses() {
return compressResponses;
}

/**
* Builder.
*
Expand All @@ -197,6 +207,7 @@ public static class Builder<T extends Builder<T>> {
protected Integer maxConnectionsCount;
protected HttpConnectorConfig httpConnectorConfig;
protected HttpsConnectorConfig httpsConnectorConfig;
protected boolean compressResponses;

public Builder httpPort(int port) {
return (T) setHttpConnector(new HttpConnectorConfig(port));
Expand Down Expand Up @@ -273,6 +284,12 @@ public T setMaxConnectionsCount(Integer maxConnectionsCount) {
return (T) this;
}

@JsonProperty("compressResponses")
public T setCompressResponses(boolean compressResponses) {
this.compressResponses = compressResponses;
return (T) this;
}

public NettyServerConfig build() {
return new NettyServerConfig(this);
}
Expand Down
2 changes: 2 additions & 0 deletions docs/user-guide/configure-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ using environment variables with the same name as the property.
jvmRouteName: "${jvm.route:noJvmRouteSet}"

proxy:
# Compress response if the client supports it. Supported formats: gzip, deflate (zlib)
compressResponses: true
connectors:
http:
# Port for accessing the proxy server over HTTP.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ case class ProxyConfig(connectors: Connectors = Connectors(HttpConnectorConfig()
requestTimeoutMillis: Int = proxyServerDefaults.requestTimeoutMillis(),
keepAliveTimeoutMillis: Int = proxyServerDefaults.keepAliveTimeoutMillis(),
maxConnectionsCount: Int = proxyServerDefaults.maxConnectionsCount(),
clientWorkerThreadsCount: Int = 1) {
clientWorkerThreadsCount: Int = 1,
compressResponses: Boolean = proxyServerDefaults.compressResponses()) {
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ case class StyxConfig(proxyConfig: ProxyConfig = ProxyConfig(),
.setNioAcceptorBacklog(proxyConfig.nioAcceptorBacklog)
.setRequestTimeoutMillis(proxyConfig.requestTimeoutMillis)
.setClientWorkerThreadsCount(proxyConfig.clientWorkerThreadsCount)
.setCompressResponses(proxyConfig.compressResponses)

val styxConfig = newStyxConfig(this.yamlText,
proxyConfigBuilder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
Copyright (C) 2013-2019 Expedia Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.hotels.styx.proxy

import com.hotels.styx.api.HttpHeaderNames.HOST
import com.hotels.styx.api.HttpRequest.get
import com.hotels.styx.api.HttpResponseStatus.OK
import com.hotels.styx.client.StyxHttpClient
import com.hotels.styx.support.StyxServerProvider
import com.hotels.styx.support.proxyHttpHostHeader
import io.kotlintest.Spec
import io.kotlintest.shouldBe
import io.kotlintest.specs.FeatureSpec
import reactor.core.publisher.toMono
import java.nio.charset.StandardCharsets.UTF_8
import java.util.zip.GZIPInputStream

class CompressionSpec : FeatureSpec() {

init {
feature("Content-type of the response is compressible") {
scenario("Compresses HTTP response if client requests gzip accept-encoding") {
val request = get("/11")
.header(HOST, styxServer().proxyHttpHostHeader())
.header("accept-encoding", "7z, gzip")
.build();

client.send(request)
.toMono()
.block()
.let {
it!!.status() shouldBe (OK)
ungzip(it.body()) shouldBe ("Hello from http server!")
}
}
scenario("Does not compress HTTP response if client did not send accept-encoding") {
val request = get("/11")
.header(HOST, styxServer().proxyHttpHostHeader())
.build();

client.send(request)
.toMono()
.block()
.let {
it!!.status() shouldBe (OK)
it.bodyAs(UTF_8) shouldBe ("Hello from http server!")
}
}

scenario("Does not compress HTTP response if unsupported accept-encoding") {
val request = get("/11")
.header(HOST, styxServer().proxyHttpHostHeader())
.header("accept-encoding", "7z")
.build();

client.send(request)
.toMono()
.block()
.let {
it!!.status() shouldBe (OK)
it.bodyAs(UTF_8) shouldBe ("Hello from http server!")
}
}

scenario("Does not compress response if already compressed (content-encoding is already set)") {
val request = get("/compressed")
.header(HOST, styxServer().proxyHttpHostHeader())
.header("accept-encoding", "gzip")
.build();

client.send(request)
.toMono()
.block()
.let {
it!!.status() shouldBe (OK)
it.bodyAs(UTF_8) shouldBe ("Hello from http server!")
}
}

}
feature("Content-type of the response is not compressible") {
scenario("Does not compress HTTP response even if accept-encoding is present") {
val request = get("/image")
.header(HOST, styxServer().proxyHttpHostHeader())
.header("accept-encoding", "gzip")
.build();

client.send(request)
.toMono()
.block()
.let {
it!!.status() shouldBe (OK)
it.bodyAs(UTF_8) shouldBe ("Hello from http server!")
}
}
}

}

private fun ungzip(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(UTF_8).use { it.readText() }


val client: StyxHttpClient = StyxHttpClient.Builder().build()

val styxServer = StyxServerProvider("""
proxy:
compressResponses: true
connectors:
http:
port: 0


admin:
connectors:
http:
port: 0

httpPipeline:
type: InterceptorPipeline
config:
handler:
type: ConditionRouter
config:
routes:
- condition: path() == "/image"
destination:
name: jpeg-image
type: StaticResponseHandler
config:
status: 200
content: "Hello from http server!"
headers:
- name: content-type
value: image/jpeg

- condition: path() == "/compressed"
destination:
name: compressed-plain-text
type: StaticResponseHandler
config:
status: 200
content: "Hello from http server!"
headers:
- name: content-type
value: text/plain
- name: content-encoding
value: gzip

fallback:
type: StaticResponseHandler
config:
status: 200
content: "Hello from http server!"
headers:
- name: content-type
value: text/plain
""".trimIndent())


override fun beforeSpec(spec: Spec) {
styxServer.restart()
}

override fun afterSpec(spec: Spec) {
styxServer.stop()
}
}