From d270e1f34dba18558025d9256d5620dadd2503e5 Mon Sep 17 00:00:00 2001 From: Jose Date: Mon, 17 Apr 2023 16:28:32 +0200 Subject: [PATCH] Support decoding of messages GZIP-compressed in REST Client Reactive Fix https://github.com/quarkusio/quarkus/issues/26981 --- .../main/asciidoc/rest-client-reactive.adoc | 3 + .../test/ClientUsingGzipCompressionTest.java | 72 +++++++++++++++++++ .../client-using-gzip-application.properties | 3 + .../runtime/RestClientBuilderImpl.java | 7 ++ .../client/impl/ClientBuilderImpl.java | 12 ++++ .../ClientGZIPDecodingInterceptor.java | 41 +++++++++++ 6 files changed, 138 insertions(+) create mode 100644 extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/ClientUsingGzipCompressionTest.java create mode 100644 extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/resources/client-using-gzip-application.properties create mode 100644 independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/interceptors/ClientGZIPDecodingInterceptor.java diff --git a/docs/src/main/asciidoc/rest-client-reactive.adoc b/docs/src/main/asciidoc/rest-client-reactive.adoc index b0f6dded8e37e..c25f0f7dffdff 100644 --- a/docs/src/main/asciidoc/rest-client-reactive.adoc +++ b/docs/src/main/asciidoc/rest-client-reactive.adoc @@ -1125,6 +1125,9 @@ The code uses the following pieces: As previously mentioned, the body parameter needs to be properly crafted by the application code to conform to the service's requirements. +=== Receiving compressed messages +REST Client Reactive also supports receiving compressed messages using GZIP. You can enable the HTTP compression support by adding the property `quarkus.http.enable-compression=true`. +When this feature is enabled and a server returns a response that includes the header `Content-Encoding: gzip`, REST Client Reactive will automatically decode the content and proceed with the message handling. == Proxy support REST Client Reactive supports sending requests through a proxy. diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/ClientUsingGzipCompressionTest.java b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/ClientUsingGzipCompressionTest.java new file mode 100644 index 0000000000000..e40abad557070 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/java/io/quarkus/rest/client/reactive/jackson/test/ClientUsingGzipCompressionTest.java @@ -0,0 +1,72 @@ +package io.quarkus.rest.client.reactive.jackson.test; + +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.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.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.vertx.http.Compressed; + +public class ClientUsingGzipCompressionTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyResource.class, Message.class, MyClient.class)) + .withConfigurationResource("client-using-gzip-application.properties"); + + @RestClient + MyClient client; + + @Test + public void testClientSupportCompressedMessagesWithGzip() { + Message actual = client.receiveCompressed(); + Assertions.assertEquals(1, actual.id); + } + + @Test + public void testClientStillWorksWhenMessageIsUncompressed() { + Message actual = client.receiveUncompressed(); + Assertions.assertEquals(1, actual.id); + } + + @Path("/client") + @RegisterRestClient(configKey = "my-client") + public interface MyClient { + + // This header is used to reproduce the issue: it will force the server to produce the payload with gzip compression + @ClientHeaderParam(name = "Accept-Encoding", value = "gzip") + @GET + @Path("/message") + Message receiveCompressed(); + + @GET + @Path("/message") + Message receiveUncompressed(); + + } + + @Path("/client") + public static class MyResource { + + @Compressed + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/message") + public String receive() { + return "{\"id\": 1}"; + } + + } + + public static class Message { + public int id; + } +} diff --git a/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/resources/client-using-gzip-application.properties b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/resources/client-using-gzip-application.properties new file mode 100644 index 0000000000000..98de043b07b81 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive-jackson/deployment/src/test/resources/client-using-gzip-application.properties @@ -0,0 +1,3 @@ +quarkus.http.enable-compression=true + +quarkus.rest-client.my-client.url=http://localhost:${quarkus.http.test-port:8081} \ No newline at end of file diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java index 105a88f0a3e2c..014bfc5f6e294 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/main/java/io/quarkus/rest/client/reactive/runtime/RestClientBuilderImpl.java @@ -47,6 +47,7 @@ public class RestClientBuilderImpl implements RestClientBuilder { private static final String DEFAULT_MAPPER_DISABLED = "microprofile.rest.client.disable.default.mapper"; private static final String TLS_TRUST_ALL = "quarkus.tls.trust-all"; + private static final String ENABLE_COMPRESSION = "quarkus.http.enable-compression"; private final ClientBuilderImpl clientBuilder = (ClientBuilderImpl) new ClientBuilderImpl() .withConfig(new ConfigurationImpl(RuntimeType.CLIENT)); @@ -341,6 +342,12 @@ public T build(Class aClass) throws IllegalStateException, RestClientDefi clientBuilder.http2((Boolean) getConfiguration().getProperty(QuarkusRestClientProperties.HTTP2)); } + Boolean enableCompression = ConfigProvider.getConfig() + .getOptionalValue(ENABLE_COMPRESSION, Boolean.class).orElse(false); + if (enableCompression) { + clientBuilder.enableCompression(); + } + if (proxyHost != null) { configureProxy(proxyHost, proxyPort, proxyUser, proxyPassword, nonProxyHosts); } else if (restClientsConfig.proxyAddress.isPresent()) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java index 025bf3e507ed4..6fd323050231e 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientBuilderImpl.java @@ -29,6 +29,7 @@ import org.jboss.logging.Logger; 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; @@ -75,6 +76,8 @@ public class ClientBuilderImpl extends ClientBuilder { private ClientLogger clientLogger = new DefaultClientLogger(); private String userAgent = "Resteasy Reactive Client"; + private boolean enableCompression; + public ClientBuilderImpl() { configuration = new ConfigurationImpl(RuntimeType.CLIENT); } @@ -188,6 +191,11 @@ public ClientBuilder clientLogger(ClientLogger clientLogger) { return this; } + public ClientBuilder enableCompression() { + this.enableCompression = true; + return this; + } + @Override public ClientImpl build() { HttpClientOptions options = Optional.ofNullable(configuration.getFromContext(HttpClientOptions.class)) @@ -273,6 +281,10 @@ public ClientImpl build() { } } + if (enableCompression) { + configuration.register(ClientGZIPDecodingInterceptor.class); + } + clientLogger.setBodySize(loggingBodySize); return new ClientImpl(options, diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/interceptors/ClientGZIPDecodingInterceptor.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/interceptors/ClientGZIPDecodingInterceptor.java new file mode 100644 index 0000000000000..fdc64e2c88c1b --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/interceptors/ClientGZIPDecodingInterceptor.java @@ -0,0 +1,41 @@ +package org.jboss.resteasy.reactive.client.interceptors; + +import static jakarta.ws.rs.core.HttpHeaders.CONTENT_ENCODING; + +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.ext.ReaderInterceptor; +import jakarta.ws.rs.ext.ReaderInterceptorContext; + +/** + * Implementation based on {@see org.jboss.resteasy.plugins.interceptors.GZIPDecodingInterceptor}. + */ +public class ClientGZIPDecodingInterceptor implements ReaderInterceptor { + + private static final String GZIP = "gzip"; + + @Override + public Object aroundReadFrom(ReaderInterceptorContext context) + throws IOException, WebApplicationException { + Object encoding = context.getHeaders().getFirst(CONTENT_ENCODING); + if (encoding != null && encoding.toString().equalsIgnoreCase(GZIP)) { + InputStream old = context.getInputStream(); + GZIPInputStream is = new GZIPInputStream(old); + context.setInputStream(is); + + Object response; + try { + response = context.proceed(); + } finally { + context.setInputStream(old); + } + + return response; + } else { + return context.proceed(); + } + } +}