diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml
index dcbd888e1c..1dd529ce0f 100644
--- a/spring-cloud-gateway-integration-tests/http2/pom.xml
+++ b/spring-cloud-gateway-integration-tests/http2/pom.xml
@@ -11,6 +11,7 @@
Spring Cloud Gateway HTTP2 Integration Test
+ 1.16.0
@@ -37,6 +38,49 @@
io.netty
netty-tcnative-boringssl-static
+
+
+ com.aayushatharva.brotli4j
+ brotli4j
+ ${brotli4j.version}
+ runtime
+
+
+ com.aayushatharva.brotli4j
+ native-linux-x86_64
+ ${brotli4j.version}
+ runtime
+
+
+ com.aayushatharva.brotli4j
+ native-linux-aarch64
+ ${brotli4j.version}
+ runtime
+
+
+ com.aayushatharva.brotli4j
+ native-linux-armv7
+ ${brotli4j.version}
+ runtime
+
+
+ com.aayushatharva.brotli4j
+ native-osx-x86_64
+ ${brotli4j.version}
+ runtime
+
+
+ com.aayushatharva.brotli4j
+ native-osx-aarch64
+ ${brotli4j.version}
+ runtime
+
+
+ com.aayushatharva.brotli4j
+ native-windows-x86_64
+ ${brotli4j.version}
+ runtime
+
org.springframework.boot
spring-boot-starter-test
@@ -56,6 +100,12 @@
assertj-core
test
+
+ commons-io
+ commons-io
+ 2.15.1
+ test
+
diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java b/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java
index 0fe5ed34c1..14c0142d0c 100644
--- a/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java
+++ b/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java
@@ -16,6 +16,7 @@
package org.springframework.cloud.gateway.tests.http2;
+import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -32,6 +33,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
@@ -52,6 +54,11 @@ public String hello() {
return "Hello";
}
+ @GetMapping("data")
+ public String getData(@RequestParam(name = "size", defaultValue = "5000", required = true) Integer size) {
+ return StringUtils.repeat("a", size);
+ }
+
@Bean
public RouteLocator myRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route(r -> r.path("/myprefix/**").filters(f -> f.stripPrefix(1)).uri("lb://myservice"))
diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml
index 8438b1d581..948fc6e4b3 100644
--- a/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml
+++ b/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml
@@ -4,6 +4,8 @@ logging:
reactor.netty.http.client: DEBUG
server:
+ compression:
+ enabled: true
ssl:
key-store: classpath:sample.jks
key-store-password: secret
diff --git a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java
index f609a031b3..583b2bde55 100644
--- a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java
+++ b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java
@@ -16,12 +16,26 @@
package org.springframework.cloud.gateway.tests.http2;
+import com.aayushatharva.brotli4j.decoder.DecoderJNI;
+import com.aayushatharva.brotli4j.decoder.DirectDecompress;
+import io.netty.handler.codec.compression.Brotli;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.time.Duration;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import java.util.List;
+import java.util.stream.Stream;
+import java.util.zip.GZIPInputStream;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.io.IOUtils;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.netty.http.Http2SslContextSpec;
@@ -63,6 +77,75 @@ public void http2Works(CapturedOutput output) {
Assertions.assertThat(output).contains("Negotiated application-level protocol [h2]", "PRI * HTTP/2.0");
}
+ @ParameterizedTest
+ @MethodSource("httpContentCompressionParameters")
+ public void httpContentCompression(String acceptEncoding, String expectedContentEncoding) throws Throwable {
+ Brotli.ensureAvailability();
+ final int uncompressedResponseSize = 6_666;
+ final String expectedUncompressedResponse = StringUtils.repeat('a', uncompressedResponseSize);
+ WebClient client = WebClient.builder().clientConnector(new ReactorClientHttpConnector(getHttpClient())).build();
+ Mono> responseEntityMono = client.get()
+ .uri(uriBuilder ->
+ uriBuilder.host("localhost").path("/myprefix/data").port(port).scheme("https").queryParam("size", uncompressedResponseSize).build())
+ .header("Accept-Encoding", acceptEncoding)
+ .retrieve()
+ .toEntity(byte[].class);
+ StepVerifier.create(responseEntityMono).assertNext(entity -> {
+ assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
+ HttpHeaders responseHeaders = entity.getHeaders();
+ assertThat(responseHeaders.getContentType().toString()).isEqualTo("text/plain;charset=UTF-8");
+ List contentEncoding = responseHeaders.get("Content-Encoding");
+ if (expectedContentEncoding == null) {
+ assertThat(contentEncoding).isNull();
+ assertThat(entity.hasBody()).isTrue();
+ assertThat(new String(entity.getBody())).isEqualTo(expectedUncompressedResponse);
+ assertThat(responseHeaders.getContentLength()).isEqualTo(uncompressedResponseSize);
+ } else {
+ assertThat(contentEncoding).containsExactly(expectedContentEncoding);
+ assertThat(entity.hasBody()).isTrue();
+ String decompressedResponseBody = new String(decompress(entity));
+ assertThat(decompressedResponseBody).isEqualTo(expectedUncompressedResponse);
+ long contentLength = responseHeaders.getContentLength();
+ assertThat(contentLength).isGreaterThan(0);
+ assertThat(contentLength).isLessThan(uncompressedResponseSize);
+ }
+ }).expectComplete().verify();
+
+ }
+
+ private static byte[] decompress(ResponseEntity entity) {
+ if (!entity.hasBody()) {
+ return null;
+ }
+ String contentEncoding = entity.getHeaders().get("Content-Encoding").get(0);
+ byte[] compressedData = entity.getBody();
+ try {
+ if ("br".equals(contentEncoding)) {
+ DirectDecompress directDecompress = DirectDecompress.decompress(compressedData);
+ assertThat(directDecompress.getResultStatus()).isEqualTo(DecoderJNI.Status.DONE);
+ return directDecompress.getDecompressedData();
+ } else if ("gzip".equals(contentEncoding)) {
+ ByteArrayInputStream input = new ByteArrayInputStream(compressedData);
+ GZIPInputStream gzInput = new GZIPInputStream(input);
+ return IOUtils.toByteArray(gzInput);
+ } else {
+ throw new IllegalStateException("unexpected contentEncoding: " + contentEncoding);
+ }
+ } catch (IOException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ public static Stream httpContentCompressionParameters() {
+ return Stream.of(Arguments.of("", null),
+ Arguments.of("br", "br"),
+ Arguments.of("gzip", "gzip"),
+ Arguments.of("br, gzip", "br"),
+ Arguments.of("gzip, br", "br"),
+ Arguments.of("garbage, gzip", "gzip"),
+ Arguments.of("garbage", null));
+ }
+
public static void assertResponse(String uri, String expected) {
WebClient client = WebClient.builder().clientConnector(new ReactorClientHttpConnector(getHttpClient())).build();
Mono> responseEntityMono = client.get().uri(uri).retrieve().toEntity(String.class);