diff --git a/.github/native-tests.json b/.github/native-tests.json index f44c910369ebd..95d36db4af702 100644 --- a/.github/native-tests.json +++ b/.github/native-tests.json @@ -92,8 +92,8 @@ }, { "category": "HTTP", - "timeout": 110, - "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface, management-interface-auth, mutiny-native-jctools", + "timeout": 120, + "test-modules": "elytron-resteasy, resteasy-jackson, elytron-resteasy-reactive, resteasy-mutiny, resteasy-reactive-kotlin/standard, vertx, vertx-http, vertx-web, vertx-http-compressors/all, vertx-http-compressors/some, vertx-web-jackson, vertx-graphql, virtual-http, rest-client, rest-client-reactive, rest-client-reactive-stork, rest-client-reactive-multipart, websockets, management-interface, management-interface-auth, mutiny-native-jctools", "os-name": "ubuntu-latest" }, { @@ -104,8 +104,8 @@ }, { "category": "Misc2", - "timeout": 70, - "test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales", + "timeout": 75, + "test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some", "os-name": "ubuntu-latest" }, { diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 27ed0305a3a88..396f2e571acda 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -69,9 +69,11 @@ The response body of a static resource is not compressed by default. You can enable the HTTP compression support by means of `quarkus.http.enable-compression=true`. If compression support is enabled then the response body is compressed if the `Content-Type` header derived from the file name of a resource is a compressed media type as configured via `quarkus.http.compress-media-types`. -TIP: By default, the following list of media types is compressed: `text/html`, `text/plain`, `text/xml`, `text/css`, `text/javascript` and `application/javascript`. +TIP: By default, the following list of media types is compressed: `text/html`, `text/plain`, `text/xml`, `text/css`, `text/javascript`, `application/javascript`, `application/graphql+json`. It means some other noteworthy media types such as `application/json`, `application/xhtml+xml` are NOT compressed by default. -NOTE: If the client does not support HTTP compression then the response body is not compressed. +NOTE: If the client does not indicate its support for HTTP compression in a request header, e.g. `Accept-Encoding: deflate, gzip, br`, then the response body is not compressed. + +TIP: Brotli compression is not available by default. You can enable it by setting `quarkus.http.compressors=deflate,gzip,br`. In case of building native image, it adds around 1MB to your executable size. [[static-resources-config]] === Other Configurations diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 4f96809afe0eb..e8e11f917310a 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.vertx.http.deployment; +import static io.quarkus.deployment.pkg.steps.GraalVM.Version.CURRENT; import static io.quarkus.runtime.TemplateHtmlBuilder.adjustRoot; import static io.quarkus.vertx.http.deployment.RequireBodyHandlerBuildItem.getBodyHandlerRequiredConditions; import static io.quarkus.vertx.http.deployment.RouteBuildItem.RouteType.FRAMEWORK_ROUTE; @@ -39,10 +40,15 @@ import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.ShutdownListenerBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.deployment.pkg.builditem.NativeImageRunnerBuildItem; +import io.quarkus.deployment.pkg.steps.GraalVM; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.deployment.pkg.steps.NoopNativeImageBuildRunner; import io.quarkus.kubernetes.spi.KubernetesPortBuildItem; import io.quarkus.netty.runtime.virtual.VirtualServerChannel; import io.quarkus.runtime.LaunchMode; @@ -50,6 +56,7 @@ import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.shutdown.ShutdownConfig; import io.quarkus.tls.TlsRegistryBuildItem; +import io.quarkus.utilities.OS; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; import io.quarkus.vertx.core.deployment.EventLoopCountBuildItem; import io.quarkus.vertx.http.HttpServerOptionsCustomizer; @@ -507,4 +514,74 @@ private static boolean isSslConfigured() { return false; } + + /** + * Compressors, deals with adding brotli compression via Brotli4J JNI wrapper. + */ + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + void brotliResources(HttpBuildTimeConfig httpBuildTimeConfig, + BuildProducer resources, + BuildProducer runtimeInitializedClasses, + NativeImageRunnerBuildItem nativeImageRunnerBuildItem) throws BuildException { + + if (httpBuildTimeConfig.compressors.isPresent() && + httpBuildTimeConfig.compressors.get().stream().anyMatch(s -> s.equalsIgnoreCase("br"))) { + final String arch = System.getProperty("os.arch"); + final boolean amd64 = arch.matches("^(amd64|x64|x86_64)$"); + final boolean aarch64 = "aarch64".equals(arch); + final String lib; + if (OS.determineOS() == OS.LINUX) { + if (amd64) { + lib = "linux-x86_64/libbrotli.so"; + } else if (aarch64) { + lib = "linux-aarch64/libbrotli.so"; + } else { + throw new BuildException("Brotli compressor: No library for linux-" + arch); + } + } else if (OS.determineOS() == OS.WINDOWS) { + if (amd64) { + lib = "windows-x86_64/brotli.dll"; + } else if (aarch64) { + lib = "windows-aarch64/brotli.dll"; + } else { + throw new BuildException("Brotli compressor: No library for windows-" + arch); + } + } else if (OS.determineOS() == OS.MAC) { + if (amd64) { + lib = "osx-x86_64/libbrotli.dylib"; + } else if (aarch64) { + lib = "osx-aarch64/libbrotli.dylib"; + } else { + throw new BuildException("Brotli compressor: No library for osx-" + arch); + } + } else { + throw new BuildException("Brotli compressor: Your platform is not supported."); + } + + resources.produce(NativeImageResourcePatternsBuildItem.builder() + // We do have Brotli4J on classpath thanks to Vert.X -> Netty dependencies. + .includePattern("\\QMETA-INF/services/com.aayushatharva.brotli4j.service.BrotliNativeProvider\\E") + // Native library. We pick only the one relevant to our system. + .includePattern("\\Qlib/" + lib + "\\E") + .build()); + + // Static initializer tries to load the native library in Brotli4jLoader; must be done at runtime. + runtimeInitializedClasses + .produce(new RuntimeInitializedClassBuildItem("com.aayushatharva.brotli4j.Brotli4jLoader")); + final GraalVM.Version v; + if (nativeImageRunnerBuildItem.getBuildRunner() instanceof NoopNativeImageBuildRunner) { + v = CURRENT; + logger.warnf("native-image is not installed. " + + "Using the default %s version as a reference to build native-sources step.", v.getVersionAsString()); + } else { + v = nativeImageRunnerBuildItem.getBuildRunner().getGraalVMVersion(); + } + // Newer 23.1+ GraalVM/Mandrel does not need this explicitly marked for runtime init thanks + // to a different strategy: https://github.com/oracle/graal/blob/vm-23.1.0/substratevm/CHANGELOG.md?plain=1#L10 + if (v.compareTo(GraalVM.Version.VERSION_23_1_0) <= 0) { + runtimeInitializedClasses + .produce(new RuntimeInitializedClassBuildItem("io.netty.handler.codec.compression.Brotli")); + } + } + } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java index 811e684bd75f6..0f766c78b12e5 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/CompressionTest.java @@ -1,18 +1,21 @@ package io.quarkus.vertx.http; +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.nullValue; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; -import org.hamcrest.Matchers; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkus.test.QuarkusUnitTest; import io.restassured.RestAssured; +import io.restassured.config.DecoderConfig; import io.vertx.core.http.HttpHeaders; import io.vertx.ext.web.Router; @@ -20,15 +23,17 @@ public class CompressionTest { private static final String APP_PROPS = "" + "quarkus.http.enable-compression=true\n"; - static String longString; - static { - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 1000; ++i) { - sb.append("Hello World;"); - } - longString = sb.toString(); - } + public static final String TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + + "incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " + + "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + + "mollit anim id est laborum." + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " + + "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + + "mollit anim id est laborum."; @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() @@ -41,15 +46,25 @@ public void test() throws Exception { // RestAssured is aware of quarkus.http.root-path // If this changes then please modify quarkus-azure-functions-http maven archetype to reflect this // in its test classes - RestAssured.given().get("/compress").then().statusCode(200) - .header("content-encoding", "gzip") - .header("content-length", Matchers.not(Matchers.equalTo(Integer.toString(longString.length())))) - .body(Matchers.equalTo(longString)); + given().get("/compress").then().statusCode(200) + .header("content-encoding", is("gzip")) + .header("content-length", Integer::parseInt, lessThan(TEXT.length())) + .body(equalTo(TEXT)); + + // Why don't you just given().header("Accept-Encoding", "deflate")? + // Because RestAssured silently ignores that and sends gzip anyway, + // search RestAssured GitHub for Accept-Encoding and decoder config. + given().config(RestAssured.config + .decoderConfig(DecoderConfig.decoderConfig().with().contentDecoders(DecoderConfig.ContentDecoder.DEFLATE))) + .get("/compress").then().statusCode(200) + .header("content-encoding", is("deflate")) + .header("content-length", Integer::parseInt, lessThan(TEXT.length())) + .body(equalTo(TEXT)); - RestAssured.given().get("/nocompress").then().statusCode(200) + given().get("/nocompress").then().statusCode(200) .header("content-encoding", is(nullValue())) - .header("content-length", Matchers.equalTo(Integer.toString(longString.length()))) - .body(Matchers.equalTo(longString)); + .header("content-length", Integer::parseInt, equalTo(TEXT.length())) + .body(equalTo(TEXT)); } @ApplicationScoped @@ -60,12 +75,12 @@ public void register(@Observes Router router) { router.route("/compress").handler(rc -> { // The content-encoding header must be removed rc.response().headers().remove(HttpHeaders.CONTENT_ENCODING); - rc.response().end(longString); + rc.response().end(TEXT); }); router.route("/nocompress").handler(rc -> { // This header is set by default // rc.response().headers().set("content-encoding", "identity"); - rc.response().end(longString); + rc.response().end(TEXT); }); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java index 5ca439bd79db9..bedb7d3ac0b26 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpBuildTimeConfig.java @@ -90,6 +90,22 @@ public class HttpBuildTimeConfig { @ConfigItem public boolean enableDecompression; + /** + * If user adds br, then brotli will be added to the list of supported compression algorithms. + * It implies loading libbrotli native library via JNI and in case of native image, + * packing the native library into the native image as a resource thus inflating its size. + * Note that a native shared object library must be available for your platform in Brotli4J project. + *

+ * Client expresses its capability by sending Accept-Encoding header, e.g. + * Accept-Encoding: deflate, gzip, br + * Server chooses the compression algorithm based on the client's capabilities and + * marks it in a response header, e.g.: + * content-encoding: gzip + * + */ + @ConfigItem(defaultValue = "gzip,deflate") + public Optional> compressors; + /** * List of media types for which the compression should be enabled automatically, unless declared explicitly via * {@link Compressed} or {@link Uncompressed}. diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java index 2ef933b783d1c..e59423d572863 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/StaticResourcesRecorder.java @@ -134,7 +134,7 @@ private boolean isCompressed(String path) { return false; } final String resourcePath = path.endsWith("/") ? path + StaticHandler.DEFAULT_INDEX_PAGE : path; - String contentType = MimeMapping.getMimeTypeForFilename(resourcePath); + final String contentType = MimeMapping.getMimeTypeForFilename(resourcePath); return contentType != null && compressMediaTypes.contains(contentType); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java index 5d9994b9c0780..36fe46a9d3aea 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/options/HttpServerOptionsUtils.java @@ -16,6 +16,10 @@ import org.jboss.logging.Logger; +import io.netty.handler.codec.compression.BrotliOptions; +import io.netty.handler.codec.compression.DeflateOptions; +import io.netty.handler.codec.compression.GzipOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.runtime.LaunchMode; @@ -274,6 +278,35 @@ public static void applyCommonOptions(HttpServerOptions httpServerOptions, httpServerOptions.setMaxInitialLineLength(httpConfiguration.limits.maxInitialLineLength); httpServerOptions.setHandle100ContinueAutomatically(httpConfiguration.handle100ContinueAutomatically); + if (buildTimeConfig.compressors.isPresent()) { + // Adding defaults too, because mere addition of .addCompressor(brotli) actually + // overrides the default deflate and gzip capability. + for (String compressor : buildTimeConfig.compressors.get()) { + if ("gzip".equalsIgnoreCase(compressor)) { + // GZip's default compression level is 6 in Netty Codec 4.1, the same + // as the default compression level in Vert.x Core 4.5.7's HttpServerOptions. + final GzipOptions defaultOps = StandardCompressionOptions.gzip(); + httpServerOptions.addCompressor(StandardCompressionOptions + .gzip(httpServerOptions.getCompressionLevel(), defaultOps.windowBits(), defaultOps.memLevel())); + } else if ("deflate".equalsIgnoreCase(compressor)) { + // Deflate's default compression level defaults the same as with GZip. + final DeflateOptions defaultOps = StandardCompressionOptions.deflate(); + httpServerOptions.addCompressor(StandardCompressionOptions + .deflate(httpServerOptions.getCompressionLevel(), defaultOps.windowBits(), defaultOps.memLevel())); + } else if ("br".equalsIgnoreCase(compressor)) { + final BrotliOptions o = StandardCompressionOptions.brotli(); + // The default compression level for brotli as of Netty Codec 4.1 is 4, + // so we don't pick up Vert.x Core 4.5.7's default of 6. User can override: + if (buildTimeConfig.compressionLevel.isPresent()) { + o.parameters().setQuality(buildTimeConfig.compressionLevel.getAsInt()); + } + httpServerOptions.addCompressor(o); + } else { + Logger.getLogger(HttpServerOptionsUtils.class).errorf("Unknown compressor: %s", compressor); + } + } + } + if (httpConfiguration.http2) { var settings = new Http2Settings(); if (httpConfiguration.limits.headerTableSize.isPresent()) { diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 52a88ee23af2c..5c1a676bc3b1d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -227,6 +227,7 @@ hibernate-orm-jpamodelgen hibernate-orm-envers vertx-http + vertx-http-compressors vertx-web vertx-web-jackson vertx diff --git a/integration-tests/vertx-http-compressors/README.md b/integration-tests/vertx-http-compressors/README.md new file mode 100644 index 0000000000000..c86acce66c984 --- /dev/null +++ b/integration-tests/vertx-http-compressors/README.md @@ -0,0 +1,12 @@ +Compressors +=========== + +All +--- + +Adds Brotli compressors, tweaks defaults. + +Some +---- + +Tests compressors in our default setting with Native Image. diff --git a/integration-tests/vertx-http-compressors/all/disable-unbind-executions b/integration-tests/vertx-http-compressors/all/disable-unbind-executions new file mode 100644 index 0000000000000..60f805fc1dfda --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/disable-unbind-executions @@ -0,0 +1 @@ +This file disables the unbind-executions profile in the quarkus-integration-tests-parent. diff --git a/integration-tests/vertx-http-compressors/all/pom.xml b/integration-tests/vertx-http-compressors/all/pom.xml new file mode 100644 index 0000000000000..2d98e432b5e6e --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-vertx-http-compressors-parent + 999-SNAPSHOT + + quarkus-integration-test-vertx-http-compressors-all + Quarkus - Integration Tests - Vertx Http Compressors - All + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-integration-test-vertx-http-compressors-app + ${project.version} + + + + + io.quarkus + quarkus-integration-test-vertx-http-compressors-app + ${project.version} + test-jar + test + + + io.vertx + vertx-web-client + test + + + io.quarkus + quarkus-junit5 + test + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/test/resources + true + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-surefire-plugin + + 1 + false + + + + + diff --git a/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllCompressedResource.java b/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllCompressedResource.java new file mode 100644 index 0000000000000..679734ef2061d --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/src/main/java/io/quarkus/compressors/it/AllCompressedResource.java @@ -0,0 +1,7 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.Path; + +@Path("/compressed") +public class AllCompressedResource extends CompressedResource { +} diff --git a/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsIT.java b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsIT.java new file mode 100644 index 0000000000000..6248970695ac7 --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsIT.java @@ -0,0 +1,7 @@ +package io.quarkus.compressors.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class RESTEndpointsIT extends RESTEndpointsTest { +} diff --git a/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java new file mode 100644 index 0000000000000..1786ff5b8e7f2 --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java @@ -0,0 +1,36 @@ +package io.quarkus.compressors.it; + +import static io.quarkus.compressors.it.Testflow.runTest; + +import java.net.URL; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class RESTEndpointsTest { + + @TestHTTPResource(value = "/compressed") + URL url; + + @ParameterizedTest + @CsvSource(value = { + //@formatter:off + // Context | Accept-Encoding | Content-Encoding | Content-Length + "/yes/text | deflate,gzip,br | br | 2316", + "/yes/text | deflate | deflate | 2402", + "/no/text | deflate,gzip,br | null | 6483", + "/yes/json | deflate | deflate | 2402", + "/no/json | deflate,gzip,br | null | 6483", + "/yes/xml | deflate,gzip,br | br | 2316", + "/no/xml | deflate,gzip,br | null | 6483", + "/yes/xhtml| deflate,gzip | gzip | 2414", + //@formatter:on + }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") + public void testCompressors(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { + runTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + } +} diff --git a/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties b/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties new file mode 100644 index 0000000000000..d7bbc1ca381a0 --- /dev/null +++ b/integration-tests/vertx-http-compressors/all/src/test/resources/application.properties @@ -0,0 +1,9 @@ +quarkus.http.enable-compression=true +# Brotli is not present by default, so we add it all here: +quarkus.http.compressors=deflate,gzip,br +# This test the level actually makes impact. When left to default, +# the test fails. +quarkus.http.compression-level=9 +# Overriding default to test it plays well with @Uncompressed and +# @Compressed annotations. +quarkus.http.compress-media-types=application/xhtml+xml,text/xml diff --git a/integration-tests/vertx-http-compressors/app/disable-unbind-executions b/integration-tests/vertx-http-compressors/app/disable-unbind-executions new file mode 100644 index 0000000000000..60f805fc1dfda --- /dev/null +++ b/integration-tests/vertx-http-compressors/app/disable-unbind-executions @@ -0,0 +1 @@ +This file disables the unbind-executions profile in the quarkus-integration-tests-parent. diff --git a/integration-tests/vertx-http-compressors/app/pom.xml b/integration-tests/vertx-http-compressors/app/pom.xml new file mode 100644 index 0000000000000..aab09d203fb9d --- /dev/null +++ b/integration-tests/vertx-http-compressors/app/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-vertx-http-compressors-parent + 999-SNAPSHOT + + quarkus-integration-test-vertx-http-compressors-app + Quarkus - Integration Tests - Vertx Http Compressors - App + + + io.vertx + vertx-web-client + test + + + io.quarkus + quarkus-junit5 + test + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + + diff --git a/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/CompressedResource.java b/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/CompressedResource.java new file mode 100644 index 0000000000000..de71039af7a9c --- /dev/null +++ b/integration-tests/vertx-http-compressors/app/src/main/java/io/quarkus/compressors/it/CompressedResource.java @@ -0,0 +1,129 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import io.quarkus.vertx.http.Compressed; +import io.quarkus.vertx.http.Uncompressed; + +public class CompressedResource { + + // If you touch this block of text, you need to adjust byte numbers in RESTEndpointsTest too. + // The text is not entirely arbitrary, it shows different compress ratios for + // Brotli and GZip while still being reasonably short. + public static final String TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + + "incididunt ut labore et minim veniam, quis nostrud exercitation" + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " + + "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + + "mollit anim id est laborum. consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " + + "dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " + + "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " + + "fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + + "mollit anim id est laborum. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia XXX " + + "❀ੈ✩‧₊˚✧˚༘ ☹#️©️®️‼️⁉️™️ℹ️↔️↕️↖️↗️↘️↙️↩️↪️⌚⌛⌨️⏏️⏩⏬⏭️⏮️⏯️⏰⏱️⏲️⏳⏸️⏹️⏺️Ⓜ️▪️▫️▶️◀️◻️◼️◽◾☀️☁️☂️☃️☄️☎️☑️☔☕☘️☝️☝☝☝☝☝☠️☢️☣️☦️☪️" + + "☮️☯️☸️☹️☺️♀️♂️♈♓♟️♠️♣️♥️♦️♨️♻️♾️♿⚒️⚓⚔️⚕️⚖️⚗️⚙️⚛️⚜️⚠️⚡⚧️⚪⚫⚰️⚱️⚽⚾⛄⛅⛈️⛎⛏️⛑️⛓️⛔⛩️⛪⛰️⛱️⛲⛳⛴️⛵⛷️⛸️⛹️⛹⛹⛹⛹⛹⛺" + + "⛽✂️✅✈️✉️✊✋✊✊✊✊✊✋✋✋✋✋✌️✌✌✌✌✌✍️✍✍✍✍✍✏️✒️✔️✖️✝️✡️✨✳️✴️❄️❇️❌❎❓❕❗❣️❤️➕➗➡️➰➿⤴️⤵️⬅️⬆️⬇️⬛⬜⭐" + + "⭕〰️〽️㊗️㊙️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️️⋆。♡˚✧˚ ༘ ⋆。♡˚ consectetur adipiscing elit. Phasellus interdum erat ligula, eget consectetur consect" + + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus interdum erat ligula, eget consectetur justo" + + "porttitor nec. Sed at est auctor, suscipit erat at, volutpat purus. Nulla facilisi. Praesent suscipit purus vel" + + "nisl pharetra, in sagittis neque tincidunt. Curabitur sit amet ligula nec nisi mollis vehicula. Morbi sit amet " + + "magna vitae arcu bibendum interdum. Vestibulum luctus felis sed tellus egestas, non suscipit risus dignissim. " + + "Integer auctor tincidunt purus, ac posuere massa tristique id. Fusce varius eu ex ut cursus. Vestibulum vehicula" + + "purus ut orci fermentum, ut feugiat dui fringilla. Ut in turpis at odio bibendum lacinia. 4231032147093284721037" + + "Duis ultrices semper velit ut varius. Nam porttitor magna sed dui vestibulum, nec bibendum tortor convallis. 666" + + "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In at augue quis " + + "justo aliquet aliquet ut non eros. Vestibulum finibus ligula magna, at euismod tortor efficitur sit amet. Aliquam" + + "erat volutpat. Curabitur bibendum orci vel risus fermentum, a gravida nulla placerat. Morbi condimentum dolor et " + + "ex finibus, et finibus magna congue. Integer et odio ut sapien aliquam pharetra. Curabitur pharetra urna id felis" + + "fringilla tincidunt. Suspendisse a erat quis enim pharetra mollis. Fusce vel est non odio tincidunt vulputate a " + + "nec sem. Donec finibus sapien sed purus tincidunt, ac venenatis felis hendrerit. Ut consectetur lacus vel urna " + + "suscipit, sed laoreet nulla volutpat.\n\r\n\r\n\r\n\t\t\t\n\r\n\r\n\r\n\r\n\t\t\t\n\r\n\r\n\r\n\r\n\t\t\t\n\r\r\r" + + "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec consequat " + + "purus a odio gravida, vel convallis justo volutpat. Integer in magna a ante porttitor ultricies at id dui. Nullam " + + "elementum sapien ut magna posuere suscipit. Proin aliquam dolor et quam suscipit bibendum. Aenean suscipit velit " + + "vel lectus posuere, ut convallis nulla consequat. In at lorem fermentum, dignissim nulla ut, consectetur enim. " + + "Maecenas vestibulum felis id justo blandit suscipit. Nulla facilisi. Duis elementum orci nec sapien accumsan, eget " + + "tempus turpis rhoncus. Quisque porttitor, mi in auctor fermentum, mi orci hendrerit risus, in dignissim erat nunc " + + "Morbi faucibus quam vel libero pharetra, non ultricies felis laoreet. Duis vulputate purus id sem interdum, vel " + + "risus gravida. Vestibulum vulputate purus non lorem ultricies varius. Donec bibendum libero a mi sollicitudin \n" + + "Praesent sed diam nec nunc pharetra malesuada nec ac urna. Nam id dui id eros vulputate pellentesque. Nulla malesuada e" + + "ros eu enim finibus, sit amet posuere leo condimentum. Curabitur in mauris lacus. Aenean nec enim ut elit bibendum suscipit ac " + + "nec nunc. Vestibulum vitae libero ac ipsum faucibus scelerisque. Curabitur ut lorem feugiat, pellentesque risus sit amet, " + + "vehicula metus. Quisque vulputate arcu et magna vehicula pharetra. Integer gravida diam et dolor hendrerit, nec suscipit odio " + + "vehicula. Donec sit amet turpis ut nulla viverra pharetra. Aenean commodo nisl ut risus fermentum vehicula. Suspendisse " + + "at augue in lorem suscipit venenatis. Morbi interdum nibh eget ex posuere, sed convallis ipsum pharetra. Aliquam auctor " + + "tincidunt urna, non dapibus risus fermentum ut. Phasellus convallis ipsum a diam consectetur, sit amet posuere erat viverra.\n" + + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Quisque non orci scelerisque, " + + "dapibus ipsum nec, laoreet magna. Suspendisse eget bibendum dui. Aenean bibendum augue vel risus laoreet faucibus. Donec " + + "quis massa sapien. Nulla facilisi. Nulla facilisi. Nullam vel varius ipsum. Cras a tincidunt libero. Fusce efficitur felis " + + "et pharetra cursus. Nullam venenatis mi nec libero auctor, nec cursus ligula dignissim. Phasellus eget libero consequat, " + + "elementum erat sed, viverra odio. Suspendisse potenti. Aenean feugiat mi a tincidunt tristique. Sed fringilla magna nec " + + "magna ultricies, id bibendum lacus faucibus. Maecenas porttitor libero sed lacus efficitur bibendum. Proin eget felis " + + "dictum, malesuada ipsum ac, finibus nisl. ipsum ac, finibus nisl. 1234567890 148130 01394829387293 932583275295 2938573592\n"; + + @GET + @Path("/yes/text") + @Produces(MediaType.TEXT_PLAIN) + @Compressed + public Response textCompressed() { + return Response.ok(TEXT).build(); + } + + @GET + @Path("/no/text") + @Produces(MediaType.TEXT_PLAIN) + @Uncompressed + public String textUncompressed() { + return TEXT; + } + + @GET + @Path("/yes/json") + @Produces(MediaType.APPLICATION_JSON) + @Compressed + public String jsonCompressed() { + // Doesn't matter the text ain't JSON, the test doesn't care. + // We need just the correct header. + return TEXT; + } + + @GET + @Path("/no/json") + @Produces(MediaType.APPLICATION_JSON) + @Uncompressed + public String jsonUncompressed() { + return TEXT; + } + + @GET + @Path("/yes/xml") + @Produces(MediaType.TEXT_XML) + // This one is compressed via default quarkus.http.compress-media-types + public String xmlCompressed() { + // Doesn't matter the text ain't XML, the test doesn't care. + // We need just the correct header. + return TEXT; + } + + @GET + @Path("/no/xml") + @Produces(MediaType.TEXT_XML) + @Uncompressed + public String xmlUncompressed() { + return TEXT; + } + + @GET + @Path("/yes/xhtml") + @Produces(MediaType.APPLICATION_XHTML_XML) + // This one is compressed quarkus.http.compress-media-types setting + public String xhtmlCompressed() { + // Doesn't matter the text ain't XML, the test doesn't care. + // We need just the correct header. + return TEXT; + } +} diff --git a/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java b/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java new file mode 100644 index 0000000000000..378486123faf9 --- /dev/null +++ b/integration-tests/vertx-http-compressors/app/src/test/java/io/quarkus/compressors/it/Testflow.java @@ -0,0 +1,127 @@ +package io.quarkus.compressors.it; + +import static io.netty.handler.codec.compression.ZlibCodecFactory.newZlibDecoder; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.quarkus.arc.ComponentsProvider.LOG; +import static io.quarkus.compressors.it.CompressedResource.TEXT; +import static java.lang.Integer.parseInt; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.BrotliDecoder; +import io.netty.handler.codec.compression.ZlibWrapper; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.client.HttpResponse; +import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.WebClientOptions; + +public class Testflow { + + // Depending on the defaults of the compression algorithm, + // the compressed content length may vary slightly between + // Vert.x/Netty versions over time. + public static final int COMPRESSION_TOLERANCE_PERCENT = 2; + + /** + * This test logic is shared by both "all" module and "some" module. + * See their RESTEndpointsTest classes. + * + * @param endpoint + * @param acceptEncoding + * @param contentEncoding + * @param contentLength + */ + public static void runTest(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { + LOG.infof("Endpoint %s; Accept-Encoding: %s; Content-Encoding: %s; Content-Length: %s", + endpoint, acceptEncoding, contentEncoding, contentLength); + // RestAssured + // ----------- + // Why not use RestAssured? Because it doesn't let you configure Accept-Encoding easily + // and when it comes to Brotli, not at all. + // No, given().header("Accept-Encoding", acceptEncoding) doesn't cut it. + final WebClient client = WebClient.create(Vertx.vertx(), new WebClientOptions() + .setLogActivity(true) + .setFollowRedirects(true) + // Vert.x Web Client + // ----------------- + // Why not use the client's built-in decompression support? + // Why you do decompression manually here? + // Because it then removes the original content-encoding header, + // and it fakes in a transfer-encoding header the server has never sent. Sad. + // RestAssured didn't let us configure Accept-Encoding easily, but at least + // it didn't mess with the response headers. + .setDecompressionSupported(false)); + final CompletableFuture> future = new CompletableFuture<>(); + client.requestAbs(HttpMethod.GET, endpoint) + .putHeader(HttpHeaders.ACCEPT_ENCODING.toString(), acceptEncoding) + .putHeader(HttpHeaders.ACCEPT.toString(), "*/*") + .putHeader(HttpHeaders.USER_AGENT.toString(), "Tester") + .send(ar -> { + if (ar.succeeded()) { + future.complete(ar.result()); + } else { + future.completeExceptionally(ar.cause()); + } + }); + try { + final HttpResponse response = future.get(); + final String actualEncoding = response.headers().get("content-encoding"); + + assertEquals(OK.code(), response.statusCode(), + "Http status must be OK."); + assertEquals(contentEncoding, actualEncoding, + "Unexpected compressor selected."); + + final int receivedLength = parseInt(response.headers().get("content-length")); + final int expectedLength = parseInt(contentLength); + + if (contentEncoding == null) { + assertEquals(expectedLength, receivedLength, + "No compression was expected, so the content-length must match exactly."); + } else { + final int expectedLengthWithTolerance = expectedLength + (expectedLength / 100 * COMPRESSION_TOLERANCE_PERCENT); + assertTrue(receivedLength <= expectedLengthWithTolerance, + "Compression apparently failed: receivedLength: " + receivedLength + + " was supposed to be less or equal to expectedLength: " + + expectedLength + " plus " + COMPRESSION_TOLERANCE_PERCENT + "% tolerance, i.e. " + + expectedLengthWithTolerance + "."); + } + + final String body; + if (actualEncoding != null && !"identity".equalsIgnoreCase(actualEncoding)) { + EmbeddedChannel channel = null; + if ("gzip".equalsIgnoreCase(actualEncoding)) { + channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.GZIP)); + } else if ("deflate".equalsIgnoreCase(actualEncoding)) { + channel = new EmbeddedChannel(newZlibDecoder(ZlibWrapper.ZLIB)); + } else if ("br".equalsIgnoreCase(actualEncoding)) { + channel = new EmbeddedChannel(new BrotliDecoder()); + } else { + fail("Unexpected compression used by server: " + actualEncoding); + } + channel.writeInbound(Unpooled.copiedBuffer(response.body().getBytes())); + channel.finish(); + final ByteBuf decompressed = channel.readInbound(); + body = decompressed.readCharSequence(decompressed.readableBytes(), StandardCharsets.UTF_8).toString(); + } else { + body = response.body().toString(StandardCharsets.UTF_8); + } + + assertEquals(TEXT, body, + "Unexpected body text."); + } catch (InterruptedException | ExecutionException e) { + fail(e); + } + } +} diff --git a/integration-tests/vertx-http-compressors/disable-unbind-executions b/integration-tests/vertx-http-compressors/disable-unbind-executions new file mode 100644 index 0000000000000..60f805fc1dfda --- /dev/null +++ b/integration-tests/vertx-http-compressors/disable-unbind-executions @@ -0,0 +1 @@ +This file disables the unbind-executions profile in the quarkus-integration-tests-parent. diff --git a/integration-tests/vertx-http-compressors/pom.xml b/integration-tests/vertx-http-compressors/pom.xml new file mode 100644 index 0000000000000..e1be2eb71f5e9 --- /dev/null +++ b/integration-tests/vertx-http-compressors/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + quarkus-integration-test-vertx-http-compressors-parent + Quarkus - Integration Tests - Vertx Http Compressors - Parent + pom + + app + all + some + + diff --git a/integration-tests/vertx-http-compressors/some/disable-unbind-executions b/integration-tests/vertx-http-compressors/some/disable-unbind-executions new file mode 100644 index 0000000000000..60f805fc1dfda --- /dev/null +++ b/integration-tests/vertx-http-compressors/some/disable-unbind-executions @@ -0,0 +1 @@ +This file disables the unbind-executions profile in the quarkus-integration-tests-parent. diff --git a/integration-tests/vertx-http-compressors/some/pom.xml b/integration-tests/vertx-http-compressors/some/pom.xml new file mode 100644 index 0000000000000..33b6515217c51 --- /dev/null +++ b/integration-tests/vertx-http-compressors/some/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + io.quarkus + quarkus-integration-test-vertx-http-compressors-parent + 999-SNAPSHOT + + quarkus-integration-test-vertx-http-compressors-some + Quarkus - Integration Tests - Vertx Http Compressors - Some + + + io.quarkus + quarkus-rest + + + io.quarkus + quarkus-integration-test-vertx-http-compressors-app + ${project.version} + + + + + io.quarkus + quarkus-integration-test-vertx-http-compressors-app + ${project.version} + test-jar + test + + + io.vertx + vertx-web-client + test + + + io.quarkus + quarkus-junit5 + test + + + + + io.quarkus + quarkus-rest-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/test/resources + true + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-surefire-plugin + + 1 + false + + + + + diff --git a/integration-tests/vertx-http-compressors/some/src/main/java/io/quarkus/compressors/it/SomeCompressedResource.java b/integration-tests/vertx-http-compressors/some/src/main/java/io/quarkus/compressors/it/SomeCompressedResource.java new file mode 100644 index 0000000000000..43845ae268229 --- /dev/null +++ b/integration-tests/vertx-http-compressors/some/src/main/java/io/quarkus/compressors/it/SomeCompressedResource.java @@ -0,0 +1,7 @@ +package io.quarkus.compressors.it; + +import jakarta.ws.rs.Path; + +@Path("/compressed") +public class SomeCompressedResource extends CompressedResource { +} diff --git a/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsIT.java b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsIT.java new file mode 100644 index 0000000000000..6248970695ac7 --- /dev/null +++ b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsIT.java @@ -0,0 +1,7 @@ +package io.quarkus.compressors.it; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class RESTEndpointsIT extends RESTEndpointsTest { +} diff --git a/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java new file mode 100644 index 0000000000000..d2f9efc94e39c --- /dev/null +++ b/integration-tests/vertx-http-compressors/some/src/test/java/io/quarkus/compressors/it/RESTEndpointsTest.java @@ -0,0 +1,35 @@ +package io.quarkus.compressors.it; + +import static io.quarkus.compressors.it.Testflow.runTest; + +import java.net.URL; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class RESTEndpointsTest { + + @TestHTTPResource(value = "/compressed") + URL url; + + @ParameterizedTest + @CsvSource(value = { + //@formatter:off + // Context | Accept-Encoding | Content-Encoding | Content-Length + "/yes/text | deflate,gzip,br | gzip | 2414", + "/yes/text | deflate | deflate | 2402", + "/no/text | deflate,gzip,br | null | 6483", + "/yes/json | deflate | deflate | 2402", + "/no/json | deflate,gzip,br | null | 6483", + "/yes/xml | deflate,gzip,br | gzip | 2414", + "/no/xml | deflate,gzip,br | null | 6483", + //@formatter:on + }, delimiter = '|', ignoreLeadingAndTrailingWhitespace = true, nullValues = "null") + public void testCompressors(String endpoint, String acceptEncoding, String contentEncoding, String contentLength) { + runTest(url.toString() + endpoint, acceptEncoding, contentEncoding, contentLength); + } +} diff --git a/integration-tests/vertx-http-compressors/some/src/test/resources/application.properties b/integration-tests/vertx-http-compressors/some/src/test/resources/application.properties new file mode 100644 index 0000000000000..45227ae0ae9bb --- /dev/null +++ b/integration-tests/vertx-http-compressors/some/src/test/resources/application.properties @@ -0,0 +1,2 @@ +# We test the defaults in this module. +quarkus.http.enable-compression=true