diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java index 5077cc4bd8..515bc6c6f0 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java @@ -30,6 +30,7 @@ import io.netty.buffer.ByteBuf; import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.compression.Brotli; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaders; @@ -522,23 +523,35 @@ public final HttpClient baseUrl(String baseUrl) { */ public final HttpClient compress(boolean compressionEnabled) { if (compressionEnabled) { + // Enabling the compression means at least "acceptGzip" is enabled. + // So we can use "acceptGzip" as a flag for the compression. if (!configuration().acceptGzip) { HttpClient dup = duplicate(); + HttpClientConfig config = dup.configuration(); HttpHeaders headers = configuration().headers.copy(); headers.add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); - dup.configuration().headers = headers; - dup.configuration().acceptGzip = true; + config.acceptGzip = true; + + if (Brotli.isAvailable()) { + headers.add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.BR); + config.acceptBrotli = true; + } + + config.headers = headers; + return dup; } } else if (configuration().acceptGzip) { HttpClient dup = duplicate(); + HttpClientConfig config = dup.configuration(); if (isCompressing(configuration().headers)) { HttpHeaders headers = configuration().headers.copy(); headers.remove(HttpHeaderNames.ACCEPT_ENCODING); - dup.configuration().headers = headers; + config.headers = headers; } - dup.configuration().acceptGzip = false; + config.acceptGzip = false; + config.acceptBrotli = false; return dup; } return this; @@ -1647,7 +1660,8 @@ public final HttpClient wiretap(boolean enable) { } static boolean isCompressing(HttpHeaders h) { - return h.contains(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP, true); + return h.contains(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP, true) + || h.contains(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.BR, true); } static String reactorNettyVersion() { diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java index 25dbf8b219..a99bc3ac2d 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java @@ -122,6 +122,7 @@ public String baseUrl() { @Override public int channelHash() { int result = super.channelHash(); + result = 31 * result + Boolean.hashCode(acceptBrotli); result = 31 * result + Boolean.hashCode(acceptGzip); result = 31 * result + Objects.hashCode(decoder); result = 31 * result + _protocols; @@ -211,6 +212,15 @@ public Http3SettingsSpec http3SettingsSpec() { return http3Settings; } + /** + * Return whether Brotli compression is enabled. + * + * @return whether Brotli compression is enabled + */ + public boolean isAcceptBrotli() { + return acceptBrotli; + } + /** * Return whether GZip compression is enabled. * @@ -331,6 +341,7 @@ public WebsocketClientSpec websocketClientSpec() { // Protected/Package private write API + boolean acceptBrotli; boolean acceptGzip; String baseUrl; BiFunction> body; @@ -367,6 +378,7 @@ public WebsocketClientSpec websocketClientSpec() { HttpClientConfig(HttpConnectionProvider connectionProvider, Map, ?> options, Supplier remoteAddress) { super(connectionProvider, options, remoteAddress); + this.acceptBrotli = false; this.acceptGzip = false; this.cookieDecoder = ClientCookieDecoder.STRICT; this.cookieEncoder = ClientCookieEncoder.STRICT; @@ -381,6 +393,7 @@ public WebsocketClientSpec websocketClientSpec() { HttpClientConfig(HttpClientConfig parent) { super(parent); + this.acceptBrotli = parent.acceptBrotli; this.acceptGzip = parent.acceptGzip; this.baseUrl = parent.baseUrl; this.body = parent.body; diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java index 6c49cd0388..3c642f1600 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java @@ -22,6 +22,9 @@ import java.lang.annotation.Target; import java.nio.charset.Charset; import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.zip.GZIPInputStream; @@ -460,7 +463,7 @@ void compressionServerDefaultClientDefaultIsNone(HttpServer server, HttpClient c @ParameterizedCompressionTest void compressionActivatedOnClientAddsHeader(HttpServer server, HttpClient client) { - AtomicReference zip = new AtomicReference<>("fail"); + AtomicReference> acceptEncodingHeaderValues = new AtomicReference<>(Collections.singletonList("fail")); disposableServer = server.compress(true) @@ -468,13 +471,15 @@ void compressionActivatedOnClientAddsHeader(HttpServer server, HttpClient client .bindNow(Duration.ofSeconds(10)); client.port(disposableServer.port()) .compress(true) - .headers(h -> zip.set(h.get("accept-encoding"))) + .headers(h -> acceptEncodingHeaderValues.set(h.getAll("accept-encoding"))) .get() .uri("/test") .responseContent() .blockLast(Duration.ofSeconds(10)); - assertThat(zip.get()).isEqualTo("gzip"); + assertThat(acceptEncodingHeaderValues.get()) + .hasSize(2) + .hasSameElementsAs(Arrays.asList("br", "gzip")); } @ParameterizedCompressionTest diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java index 973d2f33d8..b08280fb28 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java @@ -69,6 +69,7 @@ import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.unix.DomainSocketAddress; +import io.netty.handler.codec.compression.Brotli; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; @@ -132,8 +133,12 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import static io.netty.handler.codec.http.HttpHeaderNames.ACCEPT_ENCODING; +import static io.netty.handler.codec.http.HttpHeaderValues.BR; +import static io.netty.handler.codec.http.HttpHeaderValues.GZIP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assumptions.assumeThat; /** * This test class verifies {@link HttpClient}. @@ -473,6 +478,38 @@ void gzip() { .verify(Duration.ofSeconds(30)); } + @Test + void brotliEnabled() { + doTestBrotli(true); + } + + @Test + void brotliDisabled() { + doTestBrotli(false); + } + + private void doTestBrotli(boolean brotliEnabled) { + assumeThat(Brotli.isAvailable()).isTrue(); + + disposableServer = + createServer() + .compress(true) + .handle((req, res) -> res.sendString(Mono.just(req.requestHeaders().get(ACCEPT_ENCODING, "no brotli")))) + .bindNow(); + + String expectedResponse = brotliEnabled ? "br" : "no brotli"; + createHttpClientForContextWithPort() + .compress(brotliEnabled) + .headersWhen(h -> brotliEnabled ? Mono.just(h.set(ACCEPT_ENCODING, BR)) : Mono.just(h)) + .get() + .uri("/") + .responseSingle((r, buf) -> buf.asString().zipWith(Mono.just(r.status().code()))) + .as(StepVerifier::create) + .expectNextMatches(tuple -> expectedResponse.equals(tuple.getT1()) && (tuple.getT2() == 200)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + @Test void gzipEnabled() { doTestGzip(true); @@ -484,18 +521,16 @@ void gzipDisabled() { } private void doTestGzip(boolean gzipEnabled) { - String expectedResponse = gzipEnabled ? "gzip" : "no gzip"; disposableServer = createServer() - .handle((req, res) -> res.sendString(Mono.just(req.requestHeaders() - .get(HttpHeaderNames.ACCEPT_ENCODING, - "no gzip")))) + .handle((req, res) -> res.sendString(Mono.just(req.requestHeaders().get(ACCEPT_ENCODING, "no gzip")))) .bindNow(); - HttpClient client = createHttpClientForContextWithPort(); - if (gzipEnabled) { - client = client.compress(true); - } + String expectedResponse = gzipEnabled ? "gzip" : "no gzip"; + HttpClient client = + createHttpClientForContextWithPort() + .compress(gzipEnabled) + .headersWhen(h -> gzipEnabled ? Mono.just(h.set(ACCEPT_ENCODING, GZIP)) : Mono.just(h)); StepVerifier.create(client.get() .uri("/")