Skip to content

Commit

Permalink
Add Brotli support for HttpClient (#3331)
Browse files Browse the repository at this point in the history
This change is based on PR #2848
  • Loading branch information
violetagg committed Jul 3, 2024
1 parent 703bfc6 commit ee46931
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -331,6 +341,7 @@ public WebsocketClientSpec websocketClientSpec() {

// Protected/Package private write API

boolean acceptBrotli;
boolean acceptGzip;
String baseUrl;
BiFunction<? super HttpClientRequest, ? super NettyOutbound, ? extends Publisher<Void>> body;
Expand Down Expand Up @@ -367,6 +378,7 @@ public WebsocketClientSpec websocketClientSpec() {
HttpClientConfig(HttpConnectionProvider connectionProvider, Map<ChannelOption<?>, ?> options,
Supplier<? extends SocketAddress> remoteAddress) {
super(connectionProvider, options, remoteAddress);
this.acceptBrotli = false;
this.acceptGzip = false;
this.cookieDecoder = ClientCookieDecoder.STRICT;
this.cookieEncoder = ClientCookieEncoder.STRICT;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -460,21 +463,23 @@ void compressionServerDefaultClientDefaultIsNone(HttpServer server, HttpClient c

@ParameterizedCompressionTest
void compressionActivatedOnClientAddsHeader(HttpServer server, HttpClient client) {
AtomicReference<String> zip = new AtomicReference<>("fail");
AtomicReference<List<String>> acceptEncodingHeaderValues = new AtomicReference<>(Collections.singletonList("fail"));

disposableServer =
server.compress(true)
.handle((in, out) -> out.sendString(Mono.just("reply")))
.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
Expand Down Expand Up @@ -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);
Expand All @@ -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("/")
Expand Down

0 comments on commit ee46931

Please sign in to comment.