Skip to content

Commit 1ebe711

Browse files
authored
Merge pull request #31438 from mkouba/issue-31415
HTTP response compression - fix Undertow and RESTEasy Classsic support
2 parents f97a311 + 5934e09 commit 1ebe711

File tree

11 files changed

+192
-22
lines changed

11 files changed

+192
-22
lines changed

docs/src/main/asciidoc/resteasy.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ This configuration option would recognize strings in this format (shown as a reg
720720

721721
Once GZip support has been enabled you can use it on an endpoint by adding the `@org.jboss.resteasy.annotations.GZIP` annotation to your endpoint method.
722722

723-
NOTE: The configuration property `quarkus.http.enable-compression` has no effect on compression support of RESTEasy Classic endpoints.
723+
NOTE: There is also the `quarkus.http.enable-compression` configuration property which enables HTTP response compression globally. If enabled then a response body is compressed if the `Content-Type` HTTP header is set and the value is a compressed media type as configured via the `quarkus.http.compress-media-types` configuration property.
724724

725725
== Multipart Support
726726

extensions/reactive-routes/runtime/src/main/java/io/quarkus/vertx/web/runtime/HttpCompressionHandler.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ public void handle(AsyncResult<Void> result) {
3737
break;
3838
case UNDEFINED:
3939
String contentType = headers.get(HttpHeaders.CONTENT_TYPE);
40-
int paramIndex = contentType.indexOf(';');
41-
if (paramIndex > -1) {
42-
contentType = contentType.substring(0, paramIndex);
43-
}
44-
if (contentType != null
45-
&& compressedMediaTypes.contains(contentType)) {
46-
headers.remove(HttpHeaders.CONTENT_ENCODING);
40+
if (contentType != null) {
41+
int paramIndex = contentType.indexOf(';');
42+
if (paramIndex > -1) {
43+
contentType = contentType.substring(0, paramIndex);
44+
}
45+
if (compressedMediaTypes.contains(contentType)) {
46+
headers.remove(HttpHeaders.CONTENT_ENCODING);
47+
}
4748
}
4849
break;
4950
default:

extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ public void boot(ShutdownContextBuildItem shutdown,
9595
ResteasyStandaloneBuildItem standalone,
9696
Optional<RequireVirtualHttpBuildItem> requireVirtual,
9797
ExecutorBuildItem executorBuildItem,
98-
ResteasyVertxConfig resteasyVertxConfig) throws Exception {
98+
ResteasyVertxConfig resteasyVertxConfig,
99+
HttpBuildTimeConfig httpBuildTimeConfig) throws Exception {
99100

100101
if (standalone == null) {
101102
return;
@@ -105,7 +106,7 @@ public void boot(ShutdownContextBuildItem shutdown,
105106
// Handler used for both the default and non-default deployment path (specified as application path or resteasyConfig.path)
106107
// Routes use the order VertxHttpRecorder.DEFAULT_ROUTE_ORDER + 1 to ensure the default route is called before the resteasy one
107108
Handler<RoutingContext> handler = recorder.vertxRequestHandler(vertx.getVertx(),
108-
executorBuildItem.getExecutorProxy(), resteasyVertxConfig);
109+
executorBuildItem.getExecutorProxy(), resteasyVertxConfig, httpBuildTimeConfig);
109110

110111
final boolean noCustomAuthCompletionExMapper;
111112
final boolean noCustomAuthFailureExMapper;

extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/standalone/ResteasyStandaloneRecorder.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler.extractRootCause;
44

5+
import java.util.Set;
56
import java.util.concurrent.Executor;
67
import java.util.function.BiConsumer;
78
import java.util.function.Supplier;
@@ -21,6 +22,8 @@
2122
import io.quarkus.security.AuthenticationFailedException;
2223
import io.quarkus.security.AuthenticationRedirectException;
2324
import io.quarkus.security.ForbiddenException;
25+
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
26+
import io.quarkus.vertx.http.runtime.HttpCompressionHandler;
2427
import io.quarkus.vertx.http.runtime.HttpConfiguration;
2528
import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder.DefaultAuthFailureHandler;
2629
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
@@ -70,11 +73,18 @@ public void run() {
7073
}
7174

7275
public Handler<RoutingContext> vertxRequestHandler(Supplier<Vertx> vertx, Executor executor,
73-
ResteasyVertxConfig config) {
76+
ResteasyVertxConfig config, HttpBuildTimeConfig httpBuildTimeConfig) {
7477
if (deployment != null) {
75-
return new VertxRequestHandler(vertx.get(), deployment, contextPath,
78+
Handler<RoutingContext> handler = new VertxRequestHandler(vertx.get(), deployment, contextPath,
7679
new ResteasyVertxAllocator(config.responseBufferSize), executor,
7780
readTimeout.getValue().readTimeout.toMillis());
81+
82+
Set<String> compressMediaTypes = httpBuildTimeConfig.compressMediaTypes.map(Set::copyOf).orElse(Set.of());
83+
if (httpBuildTimeConfig.enableCompression && !compressMediaTypes.isEmpty()) {
84+
// If compression is enabled and the set of compressed media types is not empty then wrap the standalone handler
85+
handler = new HttpCompressionHandler(handler, compressMediaTypes);
86+
}
87+
return handler;
7888
}
7989
return null;
8090
}

extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,16 @@ public ServiceStartBuildItem boot(UndertowDeploymentRecorder recorder,
186186
ExecutorBuildItem executorBuildItem,
187187
ServletRuntimeConfig servletRuntimeConfig,
188188
ServletContextPathBuildItem servletContextPathBuildItem,
189-
Capabilities capabilities) throws Exception {
189+
Capabilities capabilities,
190+
HttpBuildTimeConfig httpBuildTimeConfig) throws Exception {
190191

191192
if (capabilities.isPresent(Capability.SECURITY)) {
192193
recorder.setupSecurity(servletDeploymentManagerBuildItem.getDeploymentManager());
193194
}
194195
Handler<RoutingContext> ut = recorder.startUndertow(shutdown, executorBuildItem.getExecutorProxy(),
195196
servletDeploymentManagerBuildItem.getDeploymentManager(),
196197
wrappers.stream().map(HttpHandlerWrapperBuildItem::getValue).collect(Collectors.toList()),
197-
servletRuntimeConfig);
198+
servletRuntimeConfig, httpBuildTimeConfig);
198199

199200
if (servletContextPathBuildItem.getServletContextPath().equals("/")) {
200201
undertowProducer.accept(new DefaultRouteBuildItem(ut));
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.quarkus.undertow.test.compress;
2+
3+
import static io.restassured.RestAssured.get;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
10+
import io.quarkus.test.QuarkusUnitTest;
11+
import io.restassured.response.ExtractableResponse;
12+
import io.restassured.response.Response;
13+
14+
public class CompressionDisabledTestCase {
15+
16+
@RegisterExtension
17+
static QuarkusUnitTest config = new QuarkusUnitTest()
18+
.withApplicationRoot(root -> root
19+
.addClasses(SimpleServlet.class))
20+
.overrideConfigKey("quarkus.http.enable-compression", "false");
21+
22+
@Test
23+
public void testCompressed() throws Exception {
24+
ExtractableResponse<Response> response = get(SimpleServlet.SERVLET_ENDPOINT)
25+
.then().statusCode(200).extract();
26+
assertTrue(response.header("Content-Encoding") == null, response.headers().toString());
27+
assertEquals("ok", response.asString());
28+
}
29+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.quarkus.undertow.test.compress;
2+
3+
import static io.restassured.RestAssured.get;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.quarkus.test.QuarkusUnitTest;
10+
11+
public class CompressionEnabledTestCase {
12+
13+
@RegisterExtension
14+
static QuarkusUnitTest config = new QuarkusUnitTest()
15+
.withApplicationRoot(root -> root
16+
.addClasses(SimpleServlet.class))
17+
.overrideConfigKey("quarkus.http.enable-compression", "true");
18+
19+
@Test
20+
public void testCompressed() throws Exception {
21+
String bodyStr = get(SimpleServlet.SERVLET_ENDPOINT).then().statusCode(200).header("Content-Encoding", "gzip").extract()
22+
.asString();
23+
assertEquals("ok", bodyStr);
24+
}
25+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.quarkus.undertow.test.compress;
2+
3+
import java.io.IOException;
4+
5+
import jakarta.servlet.ServletException;
6+
import jakarta.servlet.annotation.WebServlet;
7+
import jakarta.servlet.http.HttpServlet;
8+
import jakarta.servlet.http.HttpServletRequest;
9+
import jakarta.servlet.http.HttpServletResponse;
10+
11+
@WebServlet(urlPatterns = SimpleServlet.SERVLET_ENDPOINT)
12+
public class SimpleServlet extends HttpServlet {
13+
14+
private static final long serialVersionUID = 1L;
15+
16+
public static final String SERVLET_ENDPOINT = "/simple";
17+
18+
@Override
19+
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
20+
// this one must be listed in the quarkus.http.compress-media-types
21+
resp.setHeader("Content-type", "text/plain");
22+
resp.getWriter().write("ok");
23+
}
24+
}

extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.security.SecureRandom;
88
import java.time.Duration;
99
import java.util.ArrayList;
10+
import java.util.Collections;
1011
import java.util.EventListener;
1112
import java.util.List;
1213
import java.util.Optional;
@@ -49,6 +50,8 @@
4950
import io.quarkus.security.identity.CurrentIdentityAssociation;
5051
import io.quarkus.security.identity.SecurityIdentity;
5152
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
53+
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
54+
import io.quarkus.vertx.http.runtime.HttpCompressionHandler;
5255
import io.quarkus.vertx.http.runtime.HttpConfiguration;
5356
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;
5457
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
@@ -102,6 +105,7 @@
102105
import io.undertow.util.AttachmentKey;
103106
import io.undertow.util.ImmediateAuthenticationMechanismFactory;
104107
import io.undertow.vertx.VertxHttpExchange;
108+
import io.vertx.core.AsyncResult;
105109
import io.vertx.core.Handler;
106110
import io.vertx.ext.web.RoutingContext;
107111

@@ -348,7 +352,7 @@ public void setupSecurity(DeploymentManager manager) {
348352

349353
public Handler<RoutingContext> startUndertow(ShutdownContext shutdown, ExecutorService executorService,
350354
DeploymentManager manager, List<HandlerWrapper> wrappers,
351-
ServletRuntimeConfig servletRuntimeConfig) throws Exception {
355+
ServletRuntimeConfig servletRuntimeConfig, HttpBuildTimeConfig httpBuildTimeConfig) throws Exception {
352356

353357
shutdown.addShutdownTask(new Runnable() {
354358
@Override
@@ -382,6 +386,10 @@ public void run() {
382386
undertowOptions.set(UndertowOptions.MAX_PARAMETERS, servletRuntimeConfig.maxParameters);
383387
UndertowOptionMap undertowOptionMap = undertowOptions.getMap();
384388

389+
Set<String> compressMediaTypes = httpBuildTimeConfig.enableCompression
390+
? Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get())
391+
: Collections.emptySet();
392+
385393
return new Handler<RoutingContext>() {
386394
@Override
387395
public void handle(RoutingContext event) {
@@ -396,6 +404,20 @@ public void handle(RoutingContext event) {
396404
event.getBody());
397405
exchange.setPushHandler(VertxHttpRecorder.getRootHandler());
398406

407+
// Note that we can't add an end handler in a separate HttpCompressionHandler because VertxHttpExchange does set
408+
// its own end handler and so the end handlers added previously are just ignored...
409+
if (!compressMediaTypes.isEmpty()) {
410+
event.addEndHandler(new Handler<AsyncResult<Void>>() {
411+
412+
@Override
413+
public void handle(AsyncResult<Void> result) {
414+
if (result.succeeded()) {
415+
HttpCompressionHandler.compressIfNeeded(event, compressMediaTypes);
416+
}
417+
}
418+
});
419+
}
420+
399421
Optional<MemorySize> maxBodySize = httpConfiguration.getValue().limits.maxBodySize;
400422
if (maxBodySize.isPresent()) {
401423
exchange.setMaxEntitySize(maxBodySize.get().asLongValue());

extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,12 @@ public class HttpBuildTimeConfig {
6464
public Duration testTimeout;
6565

6666
/**
67-
* If responses should be compressed.
67+
* If enabled then the response body is compressed if the {@code Content-Type} header is set and the value is a compressed
68+
* media type as configured via {@link #compressMediaTypes}.
6869
*
69-
* Note that this will attempt to compress all responses, to avoid compressing
70-
* already compressed content (such as images) you need to set the following header:
71-
*
72-
* Content-Encoding: identity
73-
*
74-
* Which will tell vert.x not to compress the response.
70+
* Note that the RESTEasy Reactive and Reactive Routes extensions also make it possible to enable/disable compression
71+
* declaratively using the annotations {@link io.quarkus.vertx.http.Compressed} and
72+
* {@link io.quarkus.vertx.http.Uncompressed}.
7573
*/
7674
@ConfigItem
7775
public boolean enableCompression;

0 commit comments

Comments
 (0)