Skip to content

Commit

Permalink
Merge pull request quarkusio#46499 from geoand/quarkusio#46496
Browse files Browse the repository at this point in the history
Properly implement support for gzip responses in REST Client
  • Loading branch information
cescoffier authored Feb 28, 2025
2 parents 6b58d41 + 37baf0f commit 519f562
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 128 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.interceptors.ClientGZIPDecodingInterceptor;
import org.jboss.resteasy.reactive.client.spi.MissingMessageBodyReaderErrorMessageContextualizer;
import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames;
import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationStore;
Expand Down Expand Up @@ -377,17 +376,6 @@ AdditionalBeanBuildItem registerProviderBeans(CombinedIndexBuildItem combinedInd
return builder.build();
}

@BuildStep
void registerCompressionInterceptors(BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {
Boolean enableCompression = ConfigProvider.getConfig()
.getOptionalValue(ENABLE_COMPRESSION, Boolean.class)
.orElse(false);
if (enableCompression) {
reflectiveClasses.produce(ReflectiveClassBuildItem.builder(ClientGZIPDecodingInterceptor.class)
.reason(getClass().getName()).build());
}
}

@BuildStep
void handleSseEventFilter(BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/

package io.quarkus.rest.client.reactive;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
* @author <a href="http://tfox.org">Tim Fox</a>
*/
public class TestUtils {

public static String randomAlphaString(int length) {
StringBuilder builder = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char c = (char) (65 + 25 * Math.random());
builder.append(c);
}
return builder.toString();
}

public static byte[] compressGzip(String source) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gos = new GZIPOutputStream(baos);
gos.write(source.getBytes());
gos.close();
return baos.toByteArray();
}

public static byte[] decompressGzip(byte[] source) throws IOException {
GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(source));
byte[] result = gis.readAllBytes();
gis.close();
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package io.quarkus.rest.client.reactive.compression;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.netty.handler.codec.http.HttpHeaderNames;
import io.quarkus.rest.client.reactive.TestUtils;
import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.vertx.http.HttpServerOptionsCustomizer;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.codec.BodyCodec;

public class GzipCompressionTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(Endpoint.class, Client1.class, TestUtils.class))
.overrideRuntimeConfigKey("quarkus.rest-client.client1.url", "http://localhost:${quarkus.http.test-port:8081}")
.overrideRuntimeConfigKey("quarkus.rest-client.client1.enable-compression", "true")
.overrideRuntimeConfigKey("quarkus.rest-client.client2.url", "http://localhost:${quarkus.http.test-port:8081}")
.overrideRuntimeConfigKey("quarkus.rest-client.client2.enable-compression", "false");

private static final String uncompressedString;
private static final byte[] uncompressedBytes;

static {
uncompressedString = TestUtils.randomAlphaString(1000);
uncompressedBytes = uncompressedString.getBytes();
}

@Inject
Vertx vertx;

@ConfigProperty(name = "quarkus.http.test-port", defaultValue = "8081")
Integer port;

@RestClient
Client1 client1;

@RestClient
Client2 client2;

/**
* This test is very important to ensure that we know the server is actually capable of sending
* gzip encoded data
*/
@Test
void ensureServerCanSendCompressedData()
throws ExecutionException, InterruptedException, TimeoutException, IOException {
CompletableFuture<Buffer> receivedBufferCF = new CompletableFuture<>();
WebClient client = WebClient.create(vertx);
try {
client.get(port, "localhost", "/client/message")
.putHeader(HttpHeaderNames.ACCEPT_ENCODING.toString(), "gzip")
.putHeader(HttpHeaderNames.ACCEPT.toString(), "text/plain")
.as(BodyCodec.buffer())
.send()
.onFailure(receivedBufferCF::completeExceptionally)
.onSuccess(response -> {
receivedBufferCF.complete(response.bodyAsBuffer());
});
Buffer receivedBuffer = receivedBufferCF.get(10, TimeUnit.SECONDS);
byte[] receivedBytes = receivedBuffer.getBytes();
assertThat(receivedBytes).isNotEqualTo(uncompressedBytes);
assertThat(TestUtils.decompressGzip(receivedBytes)).isEqualTo(uncompressedBytes);
} finally {
client.close();
}
}

/**
* We need to know that the server can send uncompressed data as well
*/
@Test
void ensureServerCanSendUncompressedData() throws ExecutionException, InterruptedException, TimeoutException {
CompletableFuture<Buffer> receivedBufferCF = new CompletableFuture<>();
WebClient client = WebClient.create(vertx);
try {
client.get(port, "localhost", "/client/message")
.putHeader(HttpHeaderNames.ACCEPT.toString(), "text/plain")
.as(BodyCodec.buffer())
.send()
.onFailure(receivedBufferCF::completeExceptionally)
.onSuccess(response -> {
receivedBufferCF.complete(response.bodyAsBuffer());
});
Buffer receivedBuffer = receivedBufferCF.get(10, TimeUnit.SECONDS);
assertThat(receivedBuffer.getBytes()).isEqualTo(uncompressedBytes);
} finally {
client.close();
}
}

// now we can actually test the client as we know the server behaves as expected

@Test
void testReceiveCompressed() {
assertThat(client1.receiveCompressed()).isEqualTo(uncompressedString);
}

@Test
void testReceiveUncompressed() {
assertThat(client1.receiveCompressed()).isEqualTo(uncompressedString);
}

@Test
void testReceiveCompressedInClient2() throws IOException {
byte[] receivedBytes = client2.receiveCompressed();
assertThat(receivedBytes).isNotEqualTo(uncompressedBytes);
assertThat(TestUtils.decompressGzip(receivedBytes)).isEqualTo(uncompressedBytes);
}

/**
* This ensures that Vert.x will automatically compress the body of an HTTP response when the Accept-Encoding HTTP
* header requests indicates the client supports such compression
*/
@Singleton
public static class ServerOptionsCustomizer implements HttpServerOptionsCustomizer {

@Override
public void customizeHttpServer(HttpServerOptions options) {
options.setCompressionSupported(true);
}
}

/**
* We don't use Quarkus REST here as we want to make sure that don't involve any other layer that could potentially
* be adding compression. This way the compression will be provided only by Vert.x
*/
public static class Endpoint {

public void setup(@Observes Router router) {
router.route("/client/message").handler(new Handler<>() {
@Override
public void handle(RoutingContext rc) {
HttpServerRequest req = rc.request();
HttpServerResponse response = req.response().setStatusCode(200).putHeader(HttpHeaderNames.CONTENT_TYPE,
"text/plain");
Buffer body = Buffer.buffer(uncompressedBytes);
response.end(body);
}
});
}
}

/**
* This client has {@code enable-compression} set to {@code true}
*/
@Path("/client")
@RegisterRestClient(configKey = "client1")
public interface Client1 {
@ClientHeaderParam(name = "Accept-Encoding", value = "gzip")
@GET
@Path("/message")
@Produces(MediaType.TEXT_PLAIN)
String receiveCompressed();

@GET
@Path("/message")
@Produces(MediaType.TEXT_PLAIN)
String receiveUncompressed();
}

/**
* This client has {@code enable-compression} set to {@code false}
*/
@Path("/client")
@RegisterRestClient(configKey = "client2")
public interface Client2 {
@ClientHeaderParam(name = "Accept-Encoding", value = "gzip")
@GET
@Path("/message")
@Produces(MediaType.TEXT_PLAIN)
byte[] receiveCompressed();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import org.jboss.resteasy.reactive.client.TlsConfig;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.api.LoggingScope;
import org.jboss.resteasy.reactive.client.interceptors.ClientGZIPDecodingInterceptor;
import org.jboss.resteasy.reactive.client.logging.DefaultClientLogger;
import org.jboss.resteasy.reactive.client.spi.ClientContextResolver;
import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl;
Expand Down Expand Up @@ -279,7 +278,7 @@ public ClientImpl build() {
}

if (Boolean.TRUE.equals(enableCompression)) {
configuration.register(ClientGZIPDecodingInterceptor.class);
options.setDecompressionSupported(true);
}

clientLogger.setBodySize(loggingBodySize);
Expand Down
Loading

0 comments on commit 519f562

Please sign in to comment.