From 41bd91e61abae2fd8de283929ed056a5f8f638d1 Mon Sep 17 00:00:00 2001 From: raccoonback Date: Sun, 29 Dec 2024 16:59:40 +0900 Subject: [PATCH] Supports gzip, deflate, zstd compression level setting - gzip: only the range 0 to 9 is allowed - deflate: only the range 0 to 9 is allowed - zstd: only the range -7 to 22 is allowed brotli and snappy compression are supported by default. fix: https://github.com/reactor/reactor-netty/issues/3244 --- .../Http2StreamBridgeServerHandler.java | 8 +- .../reactor/netty/http/server/Http3Codec.java | 14 +- .../http/server/Http3ServerOperations.java | 4 +- .../Http3StreamBridgeServerHandler.java | 8 +- .../server/HttpCompressionSettingsSpec.java | 178 ++++++++++++++++++ .../reactor/netty/http/server/HttpServer.java | 49 +++-- .../netty/http/server/HttpServerConfig.java | 83 ++++---- .../http/server/HttpServerOperations.java | 12 +- .../netty/http/server/HttpTrafficHandler.java | 10 +- .../http/server/SimpleCompressionHandler.java | 22 +-- .../HttpCompressionClientServerTests.java | 71 ++++++- .../netty/http/server/HttpServerTests.java | 4 +- 12 files changed, 343 insertions(+), 120 deletions(-) create mode 100644 reactor-netty-http/src/main/java/reactor/netty/http/server/HttpCompressionSettingsSpec.java diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http2StreamBridgeServerHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http2StreamBridgeServerHandler.java index d143ce834..6a6251e6f 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http2StreamBridgeServerHandler.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http2StreamBridgeServerHandler.java @@ -62,7 +62,7 @@ final class Http2StreamBridgeServerHandler extends ChannelDuplexHandler { final BiPredicate compress; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; final HttpServerFormDecoderProvider formDecoderProvider; @@ -85,7 +85,7 @@ final class Http2StreamBridgeServerHandler extends ChannelDuplexHandler { Http2StreamBridgeServerHandler( @Nullable BiPredicate compress, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ServerCookieDecoder decoder, ServerCookieEncoder encoder, HttpServerFormDecoderProvider formDecoderProvider, @@ -96,7 +96,7 @@ final class Http2StreamBridgeServerHandler extends ChannelDuplexHandler { @Nullable Duration readTimeout, @Nullable Duration requestTimeout) { this.compress = compress; - this.compressionLevel = compressionLevel; + this.compressionSettings = compressionSettings; this.cookieDecoder = decoder; this.cookieEncoder = encoder; this.formDecoderProvider = formDecoderProvider; @@ -143,7 +143,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { listener, request, compress, - compressionLevel, + compressionSettings, connectionInfo, cookieDecoder, cookieEncoder, diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java index e9b8a0250..de0f6a2f9 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java @@ -63,7 +63,7 @@ final class Http3Codec extends ChannelInitializer { final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -84,7 +84,7 @@ final class Http3Codec extends ChannelInitializer { @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -103,7 +103,7 @@ final class Http3Codec extends ChannelInitializer { this.methodTagValue = methodTagValue; this.metricsRecorder = metricsRecorder; this.minCompressionSize = minCompressionSize; - this.compressionLevel = compressionLevel; + this.compressionSettings = compressionSettings; this.opsFactory = opsFactory; this.readTimeout = readTimeout; this.requestTimeout = requestTimeout; @@ -121,13 +121,13 @@ protected void initChannel(QuicStreamChannel channel) { p.addLast(NettyPipeline.H3ToHttp11Codec, new Http3FrameToHttpObjectCodec(true, validate)) .addLast(NettyPipeline.HttpTrafficHandler, - new Http3StreamBridgeServerHandler(compressPredicate, compressionLevel, cookieDecoder, cookieEncoder, formDecoderProvider, + new Http3StreamBridgeServerHandler(compressPredicate, compressionSettings, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, readTimeout, requestTimeout)); boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - p.addLast(NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionLevel)); + p.addLast(NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionSettings)); } ChannelOperations.addReactiveBridge(channel, opsFactory, listener); @@ -169,7 +169,7 @@ static ChannelHandler newHttp3ServerConnectionHandler( @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -177,7 +177,7 @@ static ChannelHandler newHttp3ServerConnectionHandler( boolean validate) { return new Http3ServerConnectionHandler( new Http3Codec(accessLogEnabled, accessLog, compressPredicate, decoder, encoder, formDecoderProvider, forwardedHeaderHandler, - httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionLevel, + httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue, validate)); } } \ No newline at end of file diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ServerOperations.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ServerOperations.java index 8db8597a9..b36bae1d5 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ServerOperations.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ServerOperations.java @@ -43,7 +43,7 @@ final class Http3ServerOperations extends HttpServerOperations { ConnectionObserver listener, HttpRequest nettyRequest, @Nullable BiPredicate compressionPredicate, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ConnectionInfo connectionInfo, ServerCookieDecoder decoder, ServerCookieEncoder encoder, @@ -55,7 +55,7 @@ final class Http3ServerOperations extends HttpServerOperations { @Nullable Duration requestTimeout, boolean secured, ZonedDateTime timestamp) { - super(c, listener, nettyRequest, compressionPredicate, compressionLevel, connectionInfo, decoder, encoder, formDecoderProvider, + super(c, listener, nettyRequest, compressionPredicate, compressionSettings, connectionInfo, decoder, encoder, formDecoderProvider, httpMessageLogFactory, isHttp2, mapHandle, readTimeout, requestTimeout, secured, timestamp, true); } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java index 2e386f4df..a33d199f8 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java @@ -54,7 +54,7 @@ final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler { final BiPredicate compress; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; final HttpServerFormDecoderProvider formDecoderProvider; @@ -75,7 +75,7 @@ final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler { Http3StreamBridgeServerHandler( @Nullable BiPredicate compress, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ServerCookieDecoder decoder, ServerCookieEncoder encoder, HttpServerFormDecoderProvider formDecoderProvider, @@ -86,7 +86,7 @@ final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler { @Nullable Duration readTimeout, @Nullable Duration requestTimeout) { this.compress = compress; - this.compressionLevel = compressionLevel; + this.compressionSettings = compressionSettings; this.cookieDecoder = decoder; this.cookieEncoder = encoder; this.formDecoderProvider = formDecoderProvider; @@ -134,7 +134,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { listener, request, compress, - compressionLevel, + compressionSettings, connectionInfo, cookieDecoder, cookieEncoder, diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpCompressionSettingsSpec.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpCompressionSettingsSpec.java new file mode 100644 index 000000000..a88701c85 --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpCompressionSettingsSpec.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.netty.http.server; + +import java.util.ArrayList; +import java.util.List; + +import io.netty.handler.codec.compression.Brotli; +import io.netty.handler.codec.compression.BrotliOptions; +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.DeflateOptions; +import io.netty.handler.codec.compression.GzipOptions; +import io.netty.handler.codec.compression.SnappyOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; +import io.netty.handler.codec.compression.Zstd; +import io.netty.handler.codec.compression.ZstdOptions; +import io.netty.util.internal.ObjectUtil; + +/** + * HTTP Compression configuration builder for the {@link SimpleCompressionHandler}. + * + * @author raccoonback + */ +public final class HttpCompressionSettingsSpec { + + private final GzipOptions gzipOptions; + private final DeflateOptions deflateOptions; + private final SnappyOptions snappyOptions; + private BrotliOptions brotliOptions; + private ZstdOptions zstdOptions; + + private HttpCompressionSettingsSpec() { + gzipOptions = StandardCompressionOptions.gzip(); + deflateOptions = StandardCompressionOptions.deflate(); + snappyOptions = StandardCompressionOptions.snappy(); + + if (Brotli.isAvailable()) { + brotliOptions = StandardCompressionOptions.brotli(); + } + + if (Zstd.isAvailable()) { + zstdOptions = StandardCompressionOptions.zstd(); + } + } + + private HttpCompressionSettingsSpec(Build build) { + gzipOptions = build.gzipOptions; + deflateOptions = build.gzipOptions; + snappyOptions = StandardCompressionOptions.snappy(); + + if (Brotli.isAvailable()) { + brotliOptions = StandardCompressionOptions.brotli(); + } + + if (Zstd.isAvailable() && build.zstdOptions != null) { + zstdOptions = build.zstdOptions; + } + } + + /** + * Creates a builder for {@link HttpCompressionSettingsSpec}. + * + * @return a new {@link HttpCompressionSettingsSpec.Builder} + */ + public static Builder builder() { + return new Build(); + } + + static HttpCompressionSettingsSpec provideDefault() { + return new HttpCompressionSettingsSpec(); + } + + CompressionOptions[] adaptToOptions() { + List options = new ArrayList<>(); + options.add(this.gzipOptions); + options.add(this.deflateOptions); + options.add(this.snappyOptions); + + if (brotliOptions != null) { + options.add(this.brotliOptions); + } + + if (zstdOptions != null) { + options.add(this.zstdOptions); + } + + return options.toArray(new CompressionOptions[0]); + } + + public interface Builder { + + /** + * Build a new {@link HttpCompressionSettingsSpec}. + * + * @return a new {@link HttpCompressionSettingsSpec} + */ + HttpCompressionSettingsSpec build(); + + /** + * Sets the gzip compression level. + * + * @return a new {@link HttpCompressionSettingsSpec.Builder} + */ + Builder gzip(int compressionLevel); + + /** + * Sets the deflate compression level. + * + * @return a new {@link HttpCompressionSettingsSpec.Builder} + */ + Builder deflate(int compressionLevel); + + /** + * Sets the zstd compression level. + * + * @return a new {@link HttpCompressionSettingsSpec.Builder} + */ + Builder zstd(int compressionLevel); + } + + private static final class Build implements Builder { + + GzipOptions gzipOptions = StandardCompressionOptions.gzip(); + DeflateOptions deflateOptions = StandardCompressionOptions.deflate(); + ZstdOptions zstdOptions; + + private static final int DEFLATE_DEFAULT_WINDOW_BITS = 15; + private static final int DEFLATE_DEFAULT_MEMORY_LEVEL = 8; + private static final int ZSTD_DEFAULT_COMPRESSION_LEVEL = 3; + private static final int ZSTD_DEFAULT_BLOCK_SIZE = 65536; + private static final int ZSTD_MAX_BLOCK_SIZE = 1 << ZSTD_DEFAULT_COMPRESSION_LEVEL + 7 + 15; + + @Override + public HttpCompressionSettingsSpec build() { + return new HttpCompressionSettingsSpec(this); + } + + @Override + public Builder gzip(int compressionLevel) { + ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel"); + + gzipOptions = StandardCompressionOptions.gzip(compressionLevel, DEFLATE_DEFAULT_WINDOW_BITS, DEFLATE_DEFAULT_MEMORY_LEVEL); + return this; + } + + @Override + public Builder deflate(int compressionLevel) { + ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel"); + + this.deflateOptions = StandardCompressionOptions.deflate(compressionLevel, DEFLATE_DEFAULT_WINDOW_BITS, DEFLATE_DEFAULT_MEMORY_LEVEL); + return this; + } + + @Override + public Builder zstd(int compressionLevel) { + if (!Zstd.isAvailable()) { + throw new IllegalStateException("Unable to set compression level on zstd."); + } + ObjectUtil.checkInRange(compressionLevel, -7, 22, "compressionLevel"); + + this.zstdOptions = StandardCompressionOptions.zstd(compressionLevel, ZSTD_DEFAULT_BLOCK_SIZE, ZSTD_MAX_BLOCK_SIZE); + return this; + } + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java index 6e12527a4..a92c341ec 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java @@ -35,7 +35,6 @@ import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.SelfSignedCertificate; -import io.netty.util.internal.ObjectUtil; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.netty.Connection; @@ -309,28 +308,9 @@ public final HttpServer compress(BiPredicate + * {@code + * HttpServer.create() + * .compress(true) + * .compressOptions( + * builder -> builder.gzip(6) + * .deflate(6) + * .zstd(3) + * ) + * .bindNow(); + * } + * + * @return a new {@link HttpServer} + */ + public final HttpServer compressOptions(Consumer compressionSettings) { + Objects.requireNonNull(compressionSettings, "compressionSettings"); + + HttpCompressionSettingsSpec.Builder builder = HttpCompressionSettingsSpec.builder(); + compressionSettings.accept(builder); + + HttpServer dup = duplicate(); + dup.configuration().compressionSettings = builder.build(); + return dup; + } + /** * Configure the * {@link ServerCookieEncoder}; {@link ServerCookieDecoder} will be diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java index aad99b4c8..4e5b56224 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java @@ -228,15 +228,6 @@ public int minCompressionSize() { return minCompressionSize; } - /** - * Returns the specified compression level. - * - * @return compression level - */ - public int compressionLevel() { - return compressionLevel; - } - /** * Return the HTTP protocol to support. Default is {@link HttpProtocol#HTTP11}. * @@ -335,7 +326,7 @@ public Function uriTagValue() { int maxKeepAliveRequests; Function methodTagValue; int minCompressionSize; - int compressionLevel; + HttpCompressionSettingsSpec compressionSettings; HttpProtocol[] protocols; int _protocols; ProxyProtocolSupportType proxyProtocolSupportType; @@ -354,7 +345,7 @@ public Function uriTagValue() { this.httpMessageLogFactory = ReactorNettyHttpMessageLogFactory.INSTANCE; this.maxKeepAliveRequests = -1; this.minCompressionSize = -1; - this.compressionLevel = 6; + this.compressionSettings = HttpCompressionSettingsSpec.provideDefault(); this.protocols = new HttpProtocol[]{HttpProtocol.HTTP11}; this._protocols = h11; this.proxyProtocolSupportType = ProxyProtocolSupportType.OFF; @@ -379,7 +370,7 @@ public Function uriTagValue() { this.maxKeepAliveRequests = parent.maxKeepAliveRequests; this.methodTagValue = parent.methodTagValue; this.minCompressionSize = parent.minCompressionSize; - this.compressionLevel = parent.compressionLevel; + this.compressionSettings = parent.compressionSettings; this.protocols = parent.protocols; this._protocols = parent._protocols; this.proxyProtocolSupportType = parent.proxyProtocolSupportType; @@ -504,7 +495,7 @@ static void addStreamHandlers(Channel ch, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -515,14 +506,14 @@ static void addStreamHandlers(Channel ch, } pipeline.addLast(NettyPipeline.H2ToHttp11Codec, HTTP2_STREAM_FRAME_TO_HTTP_OBJECT) .addLast(NettyPipeline.HttpTrafficHandler, - new Http2StreamBridgeServerHandler(compressPredicate, compressionLevel, decoder, encoder, formDecoderProvider, + new Http2StreamBridgeServerHandler(compressPredicate, compressionSettings, decoder, encoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, readTimeout, requestTimeout)); boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - pipeline.addLast(NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionLevel)); + pipeline.addLast(NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionSettings)); } ChannelOperations.addReactiveBridge(ch, opsFactory, listener); @@ -618,7 +609,7 @@ static void configureHttp3Pipeline( @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -628,7 +619,7 @@ static void configureHttp3Pipeline( p.addLast(NettyPipeline.HttpCodec, newHttp3ServerConnectionHandler(accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, - listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionLevel, opsFactory, readTimeout, + listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue, validate)); if (metricsRecorder != null) { @@ -654,7 +645,7 @@ static void configureH2Pipeline(ChannelPipeline p, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -691,7 +682,7 @@ static void configureH2Pipeline(ChannelPipeline p, .addLast(NettyPipeline.H2MultiplexHandler, new Http2MultiplexHandler(new H2Codec(accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, - mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionLevel, opsFactory, readTimeout, requestTimeout, uriTagValue))); + mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue))); IdleTimeoutHandler.addIdleTimeoutHandler(p, idleTimeout); @@ -725,7 +716,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -744,7 +735,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, Http11OrH2CleartextCodec upgrader = new Http11OrH2CleartextCodec(accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, p.get(NettyPipeline.LoggingHandler) != null, enableGracefulShutdown, formDecoderProvider, forwardedHeaderHandler, http2SettingsSpec, httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, - minCompressionSize, compressionLevel, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders()); + minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders()); ChannelHandler http2ServerHandler = new H2CleartextCodec(upgrader, http2SettingsSpec != null ? http2SettingsSpec.maxStreams() : null); @@ -759,7 +750,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, NettyPipeline.H2CUpgradeHandler, h2cUpgradeHandler) .addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.HttpTrafficHandler, - new HttpTrafficHandler(compressPredicate, compressionLevel, cookieDecoder, cookieEncoder, formDecoderProvider, + new HttpTrafficHandler(compressPredicate, compressionSettings, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, idleTimeout, listener, mapHandle, maxKeepAliveRequests, readTimeout, requestTimeout, decoder.validateHeaders())); @@ -770,7 +761,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionLevel)); + p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionSettings)); } if (metricsRecorder != null) { @@ -814,7 +805,7 @@ static void configureHttp11Pipeline(ChannelPipeline p, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @Nullable Function uriTagValue) { @@ -831,7 +822,7 @@ static void configureHttp11Pipeline(ChannelPipeline p, new HttpServerCodec(decoderConfig)) .addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.HttpTrafficHandler, - new HttpTrafficHandler(compressPredicate, compressionLevel, cookieDecoder, cookieEncoder, formDecoderProvider, + new HttpTrafficHandler(compressPredicate, compressionSettings, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, idleTimeout, listener, mapHandle, maxKeepAliveRequests, readTimeout, requestTimeout, decoder.validateHeaders())); @@ -842,7 +833,7 @@ static void configureHttp11Pipeline(ChannelPipeline p, boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionLevel)); + p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionSettings)); } if (metricsRecorder != null) { @@ -1025,7 +1016,7 @@ static final class H2Codec extends ChannelInitializer { final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -1045,7 +1036,7 @@ static final class H2Codec extends ChannelInitializer { @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -1063,7 +1054,7 @@ static final class H2Codec extends ChannelInitializer { this.methodTagValue = methodTagValue; this.metricsRecorder = metricsRecorder; this.minCompressionSize = minCompressionSize; - this.compressionLevel = compressionLevel; + this.compressionSettings = compressionSettings; this.opsFactory = opsFactory; this.readTimeout = readTimeout; this.requestTimeout = requestTimeout; @@ -1075,7 +1066,7 @@ protected void initChannel(Channel ch) { ch.pipeline().remove(this); addStreamHandlers(ch, accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, - minCompressionSize, compressionLevel, opsFactory, readTimeout, requestTimeout, uriTagValue); + minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue); } } @@ -1098,7 +1089,7 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -1121,7 +1112,7 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -1162,7 +1153,7 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer this.methodTagValue = methodTagValue; this.metricsRecorder = metricsRecorder; this.minCompressionSize = minCompressionSize; - this.compressionLevel = compressionLevel; + this.compressionSettings = compressionSettings; this.opsFactory = opsFactory; this.readTimeout = readTimeout; this.requestTimeout = requestTimeout; @@ -1177,7 +1168,7 @@ protected void initChannel(Channel ch) { ch.pipeline().remove(this); addStreamHandlers(ch, accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, methodTagValue, - metricsRecorder, minCompressionSize, compressionLevel, opsFactory, readTimeout, requestTimeout, uriTagValue); + metricsRecorder, minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue); } @Override @@ -1236,7 +1227,7 @@ static final class H2OrHttp11Codec extends ApplicationProtocolNegotiationHandler final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -1267,7 +1258,7 @@ static final class H2OrHttp11Codec extends ApplicationProtocolNegotiationHandler this.methodTagValue = initializer.methodTagValue; this.metricsRecorder = initializer.metricsRecorder; this.minCompressionSize = initializer.minCompressionSize; - this.compressionLevel = initializer.compressionLevel; + this.compressionSettings = initializer.compressionSettings; this.opsFactory = initializer.opsFactory; this.readTimeout = initializer.readTimeout; this.requestTimeout = initializer.requestTimeout; @@ -1286,7 +1277,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { configureH2Pipeline(p, accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, enableGracefulShutdown, formDecoderProvider, forwardedHeaderHandler, http2SettingsSpec, httpMessageLogFactory, idleTimeout, - listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionLevel, opsFactory, readTimeout, requestTimeout, + listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionSettings, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders()); return; } @@ -1294,7 +1285,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { if (!supportOnlyHttp2 && ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { configureHttp11Pipeline(p, accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, true, decoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, idleTimeout, listener, - mapHandle, maxKeepAliveRequests, methodTagValue, metricsRecorder, minCompressionSize, compressionLevel, readTimeout, requestTimeout, uriTagValue); + mapHandle, maxKeepAliveRequests, methodTagValue, metricsRecorder, minCompressionSize, compressionSettings, readTimeout, requestTimeout, uriTagValue); // When the server is configured with HTTP/1.1 and H2 and HTTP/1.1 is negotiated, // when channelActive event happens, this HttpTrafficHandler is still not in the pipeline, @@ -1327,7 +1318,7 @@ static final class HttpServerChannelInitializer implements ChannelPipelineConfig final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ChannelOperations.OnSetup opsFactory; final int protocols; final ProxyProtocolSupportType proxyProtocolSupportType; @@ -1355,7 +1346,7 @@ static final class HttpServerChannelInitializer implements ChannelPipelineConfig this.methodTagValue = config.methodTagValue; this.metricsRecorder = config.metricsRecorderInternal(); this.minCompressionSize = config.minCompressionSize; - this.compressionLevel = config.compressionLevel; + this.compressionSettings = config.compressionSettings; this.opsFactory = config.channelOperationsProvider(); this.protocols = config._protocols; this.proxyProtocolSupportType = config.proxyProtocolSupportType; @@ -1410,7 +1401,7 @@ else if ((protocols & h11) == h11) { methodTagValue, metricsRecorder, minCompressionSize, - compressionLevel, + compressionSettings, readTimeout, requestTimeout, uriTagValue); @@ -1442,7 +1433,7 @@ else if ((protocols & h2) == h2) { methodTagValue, metricsRecorder, minCompressionSize, - compressionLevel, + compressionSettings, opsFactory, readTimeout, requestTimeout, @@ -1466,7 +1457,7 @@ else if ((protocols & h3) == h3) { methodTagValue, metricsRecorder, minCompressionSize, - compressionLevel, + compressionSettings, opsFactory, readTimeout, requestTimeout, @@ -1496,7 +1487,7 @@ else if ((protocols & h3) == h3) { methodTagValue, metricsRecorder, minCompressionSize, - compressionLevel, + compressionSettings, opsFactory, readTimeout, requestTimeout, @@ -1522,7 +1513,7 @@ else if ((protocols & h11) == h11) { methodTagValue, metricsRecorder, minCompressionSize, - compressionLevel, + compressionSettings, readTimeout, requestTimeout, uriTagValue); @@ -1546,7 +1537,7 @@ else if ((protocols & h2c) == h2c) { methodTagValue, metricsRecorder, minCompressionSize, - compressionLevel, + compressionSettings, opsFactory, readTimeout, requestTimeout, diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java index 9740b42f1..3acffaf6a 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java @@ -138,7 +138,7 @@ class HttpServerOperations extends HttpOperations compressionPredicate; - int compressionLevel; + HttpCompressionSettingsSpec compressionSettings; boolean isWebsocket; Function> paramsResolver; String path; @@ -152,7 +152,7 @@ class HttpServerOperations extends HttpOperations compressionPredicate, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ConnectionInfo connectionInfo, ServerCookieDecoder decoder, ServerCookieEncoder encoder, @@ -195,7 +195,7 @@ class HttpServerOperations extends HttpOperations compress; - final int compressionLevel; + final HttpCompressionSettingsSpec compressionSettings; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; final HttpServerFormDecoderProvider formDecoderProvider; @@ -113,7 +113,7 @@ final class HttpTrafficHandler extends ChannelDuplexHandler implements Runnable HttpTrafficHandler( @Nullable BiPredicate compress, - int compressionLevel, + HttpCompressionSettingsSpec compressionSettings, ServerCookieDecoder decoder, ServerCookieEncoder encoder, HttpServerFormDecoderProvider formDecoderProvider, @@ -130,7 +130,7 @@ final class HttpTrafficHandler extends ChannelDuplexHandler implements Runnable this.formDecoderProvider = formDecoderProvider; this.forwardedHeaderHandler = forwardedHeaderHandler; this.compress = compress; - this.compressionLevel = compressionLevel; + this.compressionSettings = compressionSettings; this.cookieEncoder = encoder; this.cookieDecoder = decoder; this.httpMessageLogFactory = httpMessageLogFactory; @@ -246,7 +246,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { listener, request, compress, - compressionLevel, + compressionSettings, connectionInfo, cookieDecoder, cookieEncoder, @@ -599,7 +599,7 @@ public void run() { listener, nextRequest, compress, - compressionLevel, + compressionSettings, connectionInfo, cookieDecoder, cookieEncoder, diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/SimpleCompressionHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/SimpleCompressionHandler.java index f47c0d0a8..36f4872a4 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/SimpleCompressionHandler.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/SimpleCompressionHandler.java @@ -22,10 +22,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.DecoderException; -import io.netty.handler.codec.compression.Brotli; import io.netty.handler.codec.compression.CompressionOptions; -import io.netty.handler.codec.compression.StandardCompressionOptions; -import io.netty.handler.codec.compression.Zstd; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; @@ -34,7 +31,6 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.ObjectUtil; /** * {@link HttpContentCompressor} to enable on-demand compression. @@ -50,23 +46,9 @@ private SimpleCompressionHandler(CompressionOptions... options) { super(options); } - static SimpleCompressionHandler create(int compressionLevel) { - ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel"); - - List options = new ArrayList<>(); - options.add(StandardCompressionOptions.gzip(compressionLevel, 15, 8)); - options.add(StandardCompressionOptions.deflate(compressionLevel, 15, 8)); - options.add(StandardCompressionOptions.snappy()); - - if (Zstd.isAvailable()) { - options.add(StandardCompressionOptions.zstd()); - } - if (Brotli.isAvailable()) { - options.add(StandardCompressionOptions.brotli()); - } - + static SimpleCompressionHandler create(HttpCompressionSettingsSpec compressionSettings) { return new SimpleCompressionHandler( - options.toArray(new CompressionOptions[0]) + compressionSettings.adaptToOptions() ); } diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java index 1e77a6043..aaa8b2efb 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java @@ -723,15 +723,14 @@ void serverCompressionEnabledResponseCompressionDisabled(HttpServer server, Http } @ParameterizedCompressionTest - void serverCompressionEnabledByCompressionLevel(HttpServer server, HttpClient client) { + void serverCompressionWithCompressionLevelSettings(HttpServer server, HttpClient client) { disposableServer = - server.compress(true, 4) + server.compress(true) + .compressOptions(builder -> builder.gzip(4)) .handle((in, out) -> out.sendString(Mono.just("reply"))) .bindNow(Duration.ofSeconds(10)); - //don't activate compression on the client options to avoid auto-handling (which removes the header) Tuple2 resp = - //edit the header manually to attempt to trigger compression on server side client.port(disposableServer.port()) .headers(h -> h.add("accept-encoding", "gzip")) .get() @@ -761,4 +760,68 @@ void serverCompressionEnabledByCompressionLevel(HttpServer server, HttpClient cl assertThat(resp).isNotNull(); assertThat(resp.getT1()).startsWith(result); // Ignore the original data size and crc checksum comparison } + + @ParameterizedCompressionTest + void serverCompressionEnabledWithGzipCompressionLevelSettings(HttpServer server, HttpClient client) throws Exception { + disposableServer = + server.compress(true) + .compressOptions(builder -> builder.gzip(4)) + .handle((in, out) -> out.sendString(Mono.just("reply"))) + .bindNow(Duration.ofSeconds(10)); + + Tuple2 resp = + client.port(disposableServer.port()) + .headers(h -> h.add("accept-encoding", "gzip")) + .get() + .uri("/test") + .responseSingle((res, buf) -> buf.asByteArray() + .zipWith(Mono.just(res.responseHeaders()))) + .block(Duration.ofSeconds(10)); + + assertThat(resp).isNotNull(); + assertThat(resp.getT2().get("content-encoding")).isEqualTo("gzip"); + + assertThat(new String(resp.getT1(), Charset.defaultCharset())).isNotEqualTo("reply"); + + GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(resp.getT1())); + byte[] deflatedBuf = new byte[1024]; + int readable = gis.read(deflatedBuf); + gis.close(); + + assertThat(readable).isGreaterThan(0); + + String deflated = new String(deflatedBuf, 0, readable, Charset.defaultCharset()); + + assertThat(deflated).isEqualTo("reply"); + } + + @ParameterizedCompressionTest + void serverCompressionEnabledWithZstdCompressionLevel(HttpServer server, HttpClient client) { + assertThat(Zstd.isAvailable()).isTrue(); + disposableServer = + server.compress(true) + .compressOptions(builder -> builder.zstd(22)) + .handle((in, out) -> out.sendString(Mono.just("reply"))) + .bindNow(Duration.ofSeconds(10)); + + Tuple2 resp = + client.port(disposableServer.port()) + .compress(false) + .headers(h -> h.add("Accept-Encoding", "zstd")) + .get() + .uri("/test") + .responseSingle((res, buf) -> buf.asByteArray() + .zipWith(Mono.just(res.responseHeaders()))) + .block(Duration.ofSeconds(10)); + + assertThat(resp).isNotNull(); + assertThat(resp.getT2().get("content-encoding")).isEqualTo("zstd"); + + final byte[] compressedData = resp.getT1(); + assertThat(new String(compressedData, Charset.defaultCharset())).isNotEqualTo("reply"); + + final byte[] decompressedData = com.github.luben.zstd.Zstd.decompress(compressedData, 1_000); + assertThat(decompressedData).isNotEmpty(); + assertThat(new String(decompressedData, Charset.defaultCharset())).isEqualTo("reply"); + } } diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java index b428a6902..5eb027d1f 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java @@ -2214,7 +2214,7 @@ private void doTestStatus(HttpResponseStatus status) { ConnectionObserver.emptyListener(), new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"), null, - 6, + HttpCompressionSettingsSpec.provideDefault(), new ConnectionInfo(localSocketAddress, DEFAULT_HOST_NAME, DEFAULT_HTTP_PORT, remoteSocketAddress, "http", true), ServerCookieDecoder.STRICT, ServerCookieEncoder.STRICT, @@ -3273,7 +3273,7 @@ private void doTestIsFormUrlencoded(String headerValue, boolean expectation) { ConnectionObserver.emptyListener(), request, null, - 6, + HttpCompressionSettingsSpec.provideDefault(), new ConnectionInfo(localSocketAddress, DEFAULT_HOST_NAME, DEFAULT_HTTP_PORT, remoteSocketAddress, "http", true), ServerCookieDecoder.STRICT, ServerCookieEncoder.STRICT,