Skip to content

Commit

Permalink
HTTP response compression - fix Undertow and RESTEasy Classsic support
Browse files Browse the repository at this point in the history
- honor the quarkus.http.compress-media-types in Undertow Servlet and
RESTEasy Classsic extensions
- fixes #31415 and #26112
  • Loading branch information
mkouba committed Feb 27, 2023
1 parent 4bee348 commit 66fc848
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 19 deletions.
2 changes: 1 addition & 1 deletion docs/src/main/asciidoc/resteasy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@ This configuration option would recognize strings in this format (shown as a reg

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.

NOTE: The configuration property `quarkus.http.enable-compression` has no effect on compression support of RESTEasy Classic endpoints.
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.

== Multipart Support

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ public void handle(AsyncResult<Void> result) {
break;
case UNDEFINED:
String contentType = headers.get(HttpHeaders.CONTENT_TYPE);
int paramIndex = contentType.indexOf(';');
if (paramIndex > -1) {
contentType = contentType.substring(0, paramIndex);
}
if (contentType != null
&& compressedMediaTypes.contains(contentType)) {
headers.remove(HttpHeaders.CONTENT_ENCODING);
if (contentType != null) {
int paramIndex = contentType.indexOf(';');
if (paramIndex > -1) {
contentType = contentType.substring(0, paramIndex);
}
if (compressedMediaTypes.contains(contentType)) {
headers.remove(HttpHeaders.CONTENT_ENCODING);
}
}
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.util.Optional;
import java.util.Set;

import jakarta.ws.rs.ext.ExceptionMapper;

Expand Down Expand Up @@ -41,6 +42,7 @@
import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem;
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpCompressionHandler;
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
Expand Down Expand Up @@ -95,7 +97,8 @@ public void boot(ShutdownContextBuildItem shutdown,
ResteasyStandaloneBuildItem standalone,
Optional<RequireVirtualHttpBuildItem> requireVirtual,
ExecutorBuildItem executorBuildItem,
ResteasyVertxConfig resteasyVertxConfig) throws Exception {
ResteasyVertxConfig resteasyVertxConfig,
HttpBuildTimeConfig httpBuildTimeConfig) throws Exception {

if (standalone == null) {
return;
Expand All @@ -107,6 +110,12 @@ public void boot(ShutdownContextBuildItem shutdown,
Handler<RoutingContext> handler = recorder.vertxRequestHandler(vertx.getVertx(),
executorBuildItem.getExecutorProxy(), resteasyVertxConfig);

Set<String> compressMediaTypes = httpBuildTimeConfig.compressMediaTypes.map(Set::copyOf).orElse(Set.of());
if (httpBuildTimeConfig.enableCompression && !compressMediaTypes.isEmpty()) {
// If compression is enabled and the set of compressed media types is not empty then wrap the standalone handler
handler = new HttpCompressionHandler(handler, compressMediaTypes);
}

final boolean noCustomAuthCompletionExMapper;
final boolean noCustomAuthFailureExMapper;
final boolean noCustomAuthRedirectExMapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,16 @@ public ServiceStartBuildItem boot(UndertowDeploymentRecorder recorder,
ExecutorBuildItem executorBuildItem,
ServletRuntimeConfig servletRuntimeConfig,
ServletContextPathBuildItem servletContextPathBuildItem,
Capabilities capabilities) throws Exception {
Capabilities capabilities,
HttpBuildTimeConfig httpBuildTimeConfig) throws Exception {

if (capabilities.isPresent(Capability.SECURITY)) {
recorder.setupSecurity(servletDeploymentManagerBuildItem.getDeploymentManager());
}
Handler<RoutingContext> ut = recorder.startUndertow(shutdown, executorBuildItem.getExecutorProxy(),
servletDeploymentManagerBuildItem.getDeploymentManager(),
wrappers.stream().map(HttpHandlerWrapperBuildItem::getValue).collect(Collectors.toList()),
servletRuntimeConfig);
servletRuntimeConfig, httpBuildTimeConfig);

if (servletContextPathBuildItem.getServletContextPath().equals("/")) {
undertowProducer.accept(new DefaultRouteBuildItem(ut));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.undertow.test.compress;

import static io.restassured.RestAssured.get;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;

public class CompressionDisabledTestCase {

@RegisterExtension
static QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(SimpleServlet.class))
.overrideConfigKey("quarkus.http.enable-compression", "false");

@Test
public void testCompressed() throws Exception {
ExtractableResponse<Response> response = get(SimpleServlet.SERVLET_ENDPOINT)
.then().statusCode(200).extract();
assertTrue(response.header("Content-Encoding") == null, response.headers().toString());
assertEquals("ok", response.asString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.undertow.test.compress;

import static io.restassured.RestAssured.get;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class CompressionEnabledTestCase {

@RegisterExtension
static QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(SimpleServlet.class))
.overrideConfigKey("quarkus.http.enable-compression", "true");

@Test
public void testCompressed() throws Exception {
String bodyStr = get(SimpleServlet.SERVLET_ENDPOINT).then().statusCode(200).header("Content-Encoding", "gzip").extract()
.asString();
assertEquals("ok", bodyStr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.quarkus.undertow.test.compress;

import java.io.IOException;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns = SimpleServlet.SERVLET_ENDPOINT)
public class SimpleServlet extends HttpServlet {

private static final long serialVersionUID = 1L;

public static final String SERVLET_ENDPOINT = "/simple";

@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
// this one must be listed in the quarkus.http.compress-media-types
resp.setHeader("Content-type", "text/plain");
resp.getWriter().write("ok");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.security.SecureRandom;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventListener;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -49,6 +50,8 @@
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.CurrentVertxRequest;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.vertx.http.runtime.HttpCompressionHandler;
import io.quarkus.vertx.http.runtime.HttpConfiguration;
import io.quarkus.vertx.http.runtime.VertxHttpRecorder;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
Expand Down Expand Up @@ -102,6 +105,7 @@
import io.undertow.util.AttachmentKey;
import io.undertow.util.ImmediateAuthenticationMechanismFactory;
import io.undertow.vertx.VertxHttpExchange;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

Expand Down Expand Up @@ -348,7 +352,7 @@ public void setupSecurity(DeploymentManager manager) {

public Handler<RoutingContext> startUndertow(ShutdownContext shutdown, ExecutorService executorService,
DeploymentManager manager, List<HandlerWrapper> wrappers,
ServletRuntimeConfig servletRuntimeConfig) throws Exception {
ServletRuntimeConfig servletRuntimeConfig, HttpBuildTimeConfig httpBuildTimeConfig) throws Exception {

shutdown.addShutdownTask(new Runnable() {
@Override
Expand Down Expand Up @@ -382,6 +386,10 @@ public void run() {
undertowOptions.set(UndertowOptions.MAX_PARAMETERS, servletRuntimeConfig.maxParameters);
UndertowOptionMap undertowOptionMap = undertowOptions.getMap();

Set<String> compressMediaTypes = httpBuildTimeConfig.enableCompression
? Set.copyOf(httpBuildTimeConfig.compressMediaTypes.get())
: Collections.emptySet();

return new Handler<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
Expand All @@ -396,6 +404,20 @@ public void handle(RoutingContext event) {
event.getBody());
exchange.setPushHandler(VertxHttpRecorder.getRootHandler());

// Note that we can't add an end handler in a separate HttpCompressionHandler because VertxHttpExchange does set
// its own end handler and so the end handlers added previously are just ignored...
if (!compressMediaTypes.isEmpty()) {
event.addEndHandler(new Handler<AsyncResult<Void>>() {

@Override
public void handle(AsyncResult<Void> result) {
if (result.succeeded()) {
HttpCompressionHandler.compressIfNeeded(event, compressMediaTypes);
}
}
});
}

Optional<MemorySize> maxBodySize = httpConfiguration.getValue().limits.maxBodySize;
if (maxBodySize.isPresent()) {
exchange.setMaxEntitySize(maxBodySize.get().asLongValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,12 @@ public class HttpBuildTimeConfig {
public Duration testTimeout;

/**
* If responses should be compressed.
* If enabled then the response body is compressed if the {@code Content-Type} header is set and the value is a compressed
* media type as configured via {@link #compressMediaTypes}.
*
* Note that this will attempt to compress all responses, to avoid compressing
* already compressed content (such as images) you need to set the following header:
*
* Content-Encoding: identity
*
* Which will tell vert.x not to compress the response.
* Note that the RESTEasy Reactive and Reactive Routes extenions also make it possible to enable/disable compression
* declaratively using the annotations {@link io.quarkus.vertx.http.Compressed} and
* {@link io.quarkus.vertx.http.Uncompressed}.
*/
@ConfigItem
public boolean enableCompression;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.quarkus.vertx.http.runtime;

import java.util.Set;

import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;

/**
* A simple wrapping handler that removes the {@code Content-Encoding: identity} HTTP header if the {@code Content-Type}
* header is set and the value is a compressed media type as configured via
* {@link io.quarkus.vertx.http.runtime.HttpBuildTimeConfig#compressMediaTypes}.
*/
public class HttpCompressionHandler implements Handler<RoutingContext> {

private final Handler<RoutingContext> routeHandler;
private final Set<String> compressedMediaTypes;

public HttpCompressionHandler(Handler<RoutingContext> routeHandler, Set<String> compressedMediaTypes) {
this.routeHandler = routeHandler;
this.compressedMediaTypes = compressedMediaTypes;
}

@Override
public void handle(RoutingContext context) {
context.addEndHandler(new Handler<AsyncResult<Void>>() {
@Override
public void handle(AsyncResult<Void> result) {
if (result.succeeded()) {
compressIfNeeded(context, compressedMediaTypes);
}
}
});
routeHandler.handle(context);
}

public static void compressIfNeeded(RoutingContext context, Set<String> compressedMediaTypes) {
MultiMap headers = context.response().headers();
// "Content-Encoding: identity" header means that compression is enabled in the config
// and this header is set to disable the compression by default
String contentEncoding = headers.get(HttpHeaders.CONTENT_ENCODING);
if (contentEncoding != null && HttpHeaders.IDENTITY.toString().equals(contentEncoding)) {
// Just remove the header if the compression should be enabled for the current HTTP response
String contentType = headers.get(HttpHeaders.CONTENT_TYPE);
if (contentType != null) {
int paramIndex = contentType.indexOf(';');
if (paramIndex > -1) {
contentType = contentType.substring(0, paramIndex);
}
if (compressedMediaTypes.contains(contentType)) {
headers.remove(HttpHeaders.CONTENT_ENCODING);
}
}
}
}

}

0 comments on commit 66fc848

Please sign in to comment.