diff --git a/docs/modules/ROOT/pages/http-server.adoc b/docs/modules/ROOT/pages/http-server.adoc index c4c64fb42f..c27fd9a040 100644 --- a/docs/modules/ROOT/pages/http-server.adoc +++ b/docs/modules/ROOT/pages/http-server.adoc @@ -162,6 +162,28 @@ You can configure the `HTTP` server to send a compressed response, depending on * `compress(int)`: The compression is performed once the response size exceeds the given value (in bytes). * `compress(BiPredicate)`: The compression is performed if the predicate returns `true`. +* `compressOptions(HttpCompressionOption... compressionOptions)`: Specifies the compression options for GZip, Deflate, and ZSTD. ++ +[NOTE] +==== +GZip Compression Options + +* compression level : only the range 0 to 9 is allowed. (default: 6) +* window bits : only the range 0 to 9 is allowed. (default: 15) +* memory level : only the range 1 to 9 is allowed. (default: 8) + +Deflate Compression Options + +* compression level : only the range 0 to 9 is allowed. (default: 6) +* window bits : only the range 0 to 9 is allowed. (default: 15) +* memory level : only the range 1 to 9 is allowed. (default: 8) + +ZSTD Compression Options + +* compression level : only the range -131072 to 9 is allowed. (default: 3) +* block size : only the positive number is allowed. (default: 65536, that is 64KB) +* max encode size : only the positive number is allowed. (default: 33554432, that is 32MB) +==== The following example uses the `compress` method (set to `true`) to enable compression: 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 6adbb42d34..9ea5eba9cb 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2025 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. @@ -47,6 +47,7 @@ import reactor.netty.ReactorNetty; import reactor.netty.http.logging.HttpMessageArgProviderFactory; import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.util.annotation.Nullable; import static io.netty.handler.codec.http.LastHttpContent.EMPTY_LAST_CONTENT; @@ -62,6 +63,7 @@ final class Http2StreamBridgeServerHandler extends ChannelDuplexHandler { final BiPredicate compress; + final HttpCompressionOptionsSpec compressionOptions; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; final HttpServerFormDecoderProvider formDecoderProvider; @@ -84,6 +86,7 @@ final class Http2StreamBridgeServerHandler extends ChannelDuplexHandler { Http2StreamBridgeServerHandler( @Nullable BiPredicate compress, + @Nullable HttpCompressionOptionsSpec compressionOptions, ServerCookieDecoder decoder, ServerCookieEncoder encoder, HttpServerFormDecoderProvider formDecoderProvider, @@ -94,6 +97,7 @@ final class Http2StreamBridgeServerHandler extends ChannelDuplexHandler { @Nullable Duration readTimeout, @Nullable Duration requestTimeout) { this.compress = compress; + this.compressionOptions = compressionOptions; this.cookieDecoder = decoder; this.cookieEncoder = encoder; this.formDecoderProvider = formDecoderProvider; @@ -140,6 +144,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { listener, request, compress, + compressionOptions, 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 5378628d01..450ed85194 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2024-2025 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. @@ -31,6 +31,7 @@ import reactor.netty.channel.ChannelMetricsRecorder; import reactor.netty.channel.ChannelOperations; import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.netty.http.server.logging.AccessLog; import reactor.netty.http.server.logging.AccessLogArgProvider; import reactor.netty.http.server.logging.AccessLogHandlerFactory; @@ -63,6 +64,7 @@ final class Http3Codec extends ChannelInitializer { final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; + final HttpCompressionOptionsSpec compressionOptions; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -83,6 +85,7 @@ final class Http3Codec extends ChannelInitializer { @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -101,6 +104,7 @@ final class Http3Codec extends ChannelInitializer { this.methodTagValue = methodTagValue; this.metricsRecorder = metricsRecorder; this.minCompressionSize = minCompressionSize; + this.compressionOptions = compressionOptions; this.opsFactory = opsFactory; this.readTimeout = readTimeout; this.requestTimeout = requestTimeout; @@ -118,13 +122,13 @@ protected void initChannel(QuicStreamChannel channel) { p.addLast(NettyPipeline.H3ToHttp11Codec, new Http3FrameToHttpObjectCodec(true, validate)) .addLast(NettyPipeline.HttpTrafficHandler, - new Http3StreamBridgeServerHandler(compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, + new Http3StreamBridgeServerHandler(compressPredicate, compressionOptions, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, readTimeout, requestTimeout)); boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - p.addLast(NettyPipeline.CompressionHandler, new SimpleCompressionHandler()); + p.addLast(NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionOptions)); } ChannelOperations.addReactiveBridge(channel, opsFactory, listener); @@ -166,6 +170,7 @@ static ChannelHandler newHttp3ServerConnectionHandler( @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -173,7 +178,7 @@ static ChannelHandler newHttp3ServerConnectionHandler( boolean validate) { return new Http3ServerConnectionHandler( new Http3Codec(accessLogEnabled, accessLog, compressPredicate, decoder, encoder, formDecoderProvider, forwardedHeaderHandler, - httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, + httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionOptions, 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 deaa4ac6ee..695b265c18 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2024-2025 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. @@ -24,6 +24,7 @@ import reactor.netty.Connection; import reactor.netty.ConnectionObserver; import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.util.annotation.Nullable; import java.net.SocketAddress; @@ -43,6 +44,7 @@ final class Http3ServerOperations extends HttpServerOperations { ConnectionObserver listener, HttpRequest nettyRequest, @Nullable BiPredicate compressionPredicate, + @Nullable HttpCompressionOptionsSpec compressionOptions, ConnectionInfo connectionInfo, ServerCookieDecoder decoder, ServerCookieEncoder encoder, @@ -54,7 +56,7 @@ final class Http3ServerOperations extends HttpServerOperations { @Nullable Duration requestTimeout, boolean secured, ZonedDateTime timestamp) { - super(c, listener, nettyRequest, compressionPredicate, connectionInfo, decoder, encoder, formDecoderProvider, + super(c, listener, nettyRequest, compressionPredicate, compressionOptions, 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 ff0cb00a85..0066a824a4 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2024-2025 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. @@ -41,6 +41,7 @@ import reactor.netty.ReactorNetty; import reactor.netty.http.logging.HttpMessageArgProviderFactory; import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.util.annotation.Nullable; import java.net.SocketAddress; @@ -54,6 +55,7 @@ final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler { final BiPredicate compress; + final HttpCompressionOptionsSpec compressionOptions; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; final HttpServerFormDecoderProvider formDecoderProvider; @@ -74,6 +76,7 @@ final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler { Http3StreamBridgeServerHandler( @Nullable BiPredicate compress, + @Nullable HttpCompressionOptionsSpec compressionOptions, ServerCookieDecoder decoder, ServerCookieEncoder encoder, HttpServerFormDecoderProvider formDecoderProvider, @@ -84,6 +87,7 @@ final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler { @Nullable Duration readTimeout, @Nullable Duration requestTimeout) { this.compress = compress; + this.compressionOptions = compressionOptions; this.cookieDecoder = decoder; this.cookieEncoder = encoder; this.formDecoderProvider = formDecoderProvider; @@ -131,6 +135,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { listener, request, compress, + compressionOptions, connectionInfo, cookieDecoder, cookieEncoder, 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 b8128dde7e..1d9d1bbd00 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2025 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. @@ -45,6 +45,8 @@ import reactor.netty.http.HttpProtocol; import reactor.netty.http.logging.HttpMessageLogFactory; import reactor.netty.http.logging.ReactorNettyHttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOption; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.netty.http.server.logging.AccessLog; import reactor.netty.http.server.logging.AccessLogArgProvider; import reactor.netty.http.server.logging.AccessLogFactory; @@ -301,6 +303,7 @@ public final HttpServer compress(BiPredicate + * {@code + * HttpServer.create() + * .compress(true) + * .compressOptions( + * GzipOption.builder() + * .compressionLevel(6) + * .windowBits(15) + * .memoryLevel(8) + * .build(), + * ZstdOption.builder() + * .compressionLevel(3) + * .build() + * ) + * .bindNow(); + * } + * + * @return a new {@link HttpServer} + */ + public final HttpServer compressOptions(HttpCompressionOption... compressionOptions) { + Objects.requireNonNull(compressionOptions, "compressionOptions"); + + HttpServer dup = duplicate(); + dup.configuration().compressionOptions = new HttpCompressionOptionsSpec(compressionOptions); + 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 5d6a8f89a8..7f098f6d1b 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2025 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. @@ -68,6 +68,7 @@ import reactor.netty.http.HttpResources; import reactor.netty.http.logging.HttpMessageLogFactory; import reactor.netty.http.logging.ReactorNettyHttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.netty.http.server.logging.AccessLog; import reactor.netty.http.server.logging.AccessLogArgProvider; import reactor.netty.http.server.logging.AccessLogHandlerFactory; @@ -326,6 +327,7 @@ public Function uriTagValue() { int maxKeepAliveRequests; Function methodTagValue; int minCompressionSize; + HttpCompressionOptionsSpec compressionOptions; HttpProtocol[] protocols; int _protocols; ProxyProtocolSupportType proxyProtocolSupportType; @@ -344,6 +346,7 @@ public Function uriTagValue() { this.httpMessageLogFactory = ReactorNettyHttpMessageLogFactory.INSTANCE; this.maxKeepAliveRequests = -1; this.minCompressionSize = -1; + this.compressionOptions = null; this.protocols = new HttpProtocol[]{HttpProtocol.HTTP11}; this._protocols = h11; this.proxyProtocolSupportType = ProxyProtocolSupportType.OFF; @@ -368,6 +371,7 @@ public Function uriTagValue() { this.maxKeepAliveRequests = parent.maxKeepAliveRequests; this.methodTagValue = parent.methodTagValue; this.minCompressionSize = parent.minCompressionSize; + this.compressionOptions = parent.compressionOptions; this.protocols = parent.protocols; this._protocols = parent._protocols; this.proxyProtocolSupportType = parent.proxyProtocolSupportType; @@ -492,6 +496,7 @@ static void addStreamHandlers(Channel ch, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -502,14 +507,14 @@ static void addStreamHandlers(Channel ch, } pipeline.addLast(NettyPipeline.H2ToHttp11Codec, HTTP2_STREAM_FRAME_TO_HTTP_OBJECT) .addLast(NettyPipeline.HttpTrafficHandler, - new Http2StreamBridgeServerHandler(compressPredicate, decoder, encoder, formDecoderProvider, + new Http2StreamBridgeServerHandler(compressPredicate, compressionOptions, decoder, encoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, readTimeout, requestTimeout)); boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - pipeline.addLast(NettyPipeline.CompressionHandler, new SimpleCompressionHandler()); + pipeline.addLast(NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionOptions)); } ChannelOperations.addReactiveBridge(ch, opsFactory, listener); @@ -605,6 +610,7 @@ static void configureHttp3Pipeline( @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -614,7 +620,7 @@ static void configureHttp3Pipeline( p.addLast(NettyPipeline.HttpCodec, newHttp3ServerConnectionHandler(accessLogEnabled, accessLog, compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, - listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout, + listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionOptions, opsFactory, readTimeout, requestTimeout, uriTagValue, validate)); if (metricsRecorder != null) { @@ -640,6 +646,7 @@ static void configureH2Pipeline(ChannelPipeline p, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -676,7 +683,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, opsFactory, readTimeout, requestTimeout, uriTagValue))); + mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionOptions, opsFactory, readTimeout, requestTimeout, uriTagValue))); IdleTimeoutHandler.addIdleTimeoutHandler(p, idleTimeout); @@ -710,6 +717,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -728,7 +736,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, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders()); + minCompressionSize, compressionOptions, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders()); ChannelHandler http2ServerHandler = new H2CleartextCodec(upgrader, http2SettingsSpec != null ? http2SettingsSpec.maxStreams() : null); @@ -743,7 +751,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, NettyPipeline.H2CUpgradeHandler, h2cUpgradeHandler) .addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.HttpTrafficHandler, - new HttpTrafficHandler(compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, + new HttpTrafficHandler(compressPredicate, compressionOptions, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, idleTimeout, listener, mapHandle, maxKeepAliveRequests, readTimeout, requestTimeout, decoder.validateHeaders())); @@ -754,7 +762,7 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, new SimpleCompressionHandler()); + p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionOptions)); } if (metricsRecorder != null) { @@ -798,6 +806,7 @@ static void configureHttp11Pipeline(ChannelPipeline p, @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @Nullable Function uriTagValue) { @@ -814,7 +823,7 @@ static void configureHttp11Pipeline(ChannelPipeline p, new HttpServerCodec(decoderConfig)) .addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.HttpTrafficHandler, - new HttpTrafficHandler(compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, + new HttpTrafficHandler(compressPredicate, compressionOptions, cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, idleTimeout, listener, mapHandle, maxKeepAliveRequests, readTimeout, requestTimeout, decoder.validateHeaders())); @@ -825,7 +834,7 @@ static void configureHttp11Pipeline(ChannelPipeline p, boolean alwaysCompress = compressPredicate == null && minCompressionSize == 0; if (alwaysCompress) { - p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, new SimpleCompressionHandler()); + p.addBefore(NettyPipeline.HttpTrafficHandler, NettyPipeline.CompressionHandler, SimpleCompressionHandler.create(compressionOptions)); } if (metricsRecorder != null) { @@ -1008,6 +1017,7 @@ static final class H2Codec extends ChannelInitializer { final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; + final HttpCompressionOptionsSpec compressionOptions; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -1027,6 +1037,7 @@ static final class H2Codec extends ChannelInitializer { @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -1044,6 +1055,7 @@ static final class H2Codec extends ChannelInitializer { this.methodTagValue = methodTagValue; this.metricsRecorder = metricsRecorder; this.minCompressionSize = minCompressionSize; + this.compressionOptions = compressionOptions; this.opsFactory = opsFactory; this.readTimeout = readTimeout; this.requestTimeout = requestTimeout; @@ -1055,7 +1067,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, opsFactory, readTimeout, requestTimeout, uriTagValue); + minCompressionSize, compressionOptions, opsFactory, readTimeout, requestTimeout, uriTagValue); } } @@ -1078,6 +1090,7 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; + final HttpCompressionOptionsSpec compressionOptions; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -1100,6 +1113,7 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer @Nullable Function methodTagValue, @Nullable ChannelMetricsRecorder metricsRecorder, int minCompressionSize, + @Nullable HttpCompressionOptionsSpec compressionOptions, ChannelOperations.OnSetup opsFactory, @Nullable Duration readTimeout, @Nullable Duration requestTimeout, @@ -1140,6 +1154,7 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer this.methodTagValue = methodTagValue; this.metricsRecorder = metricsRecorder; this.minCompressionSize = minCompressionSize; + this.compressionOptions = compressionOptions; this.opsFactory = opsFactory; this.readTimeout = readTimeout; this.requestTimeout = requestTimeout; @@ -1154,7 +1169,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, opsFactory, readTimeout, requestTimeout, uriTagValue); + metricsRecorder, minCompressionSize, compressionOptions, opsFactory, readTimeout, requestTimeout, uriTagValue); } @Override @@ -1213,6 +1228,7 @@ static final class H2OrHttp11Codec extends ApplicationProtocolNegotiationHandler final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; + final HttpCompressionOptionsSpec compressionOptions; final ChannelOperations.OnSetup opsFactory; final Duration readTimeout; final Duration requestTimeout; @@ -1243,6 +1259,7 @@ static final class H2OrHttp11Codec extends ApplicationProtocolNegotiationHandler this.methodTagValue = initializer.methodTagValue; this.metricsRecorder = initializer.metricsRecorder; this.minCompressionSize = initializer.minCompressionSize; + this.compressionOptions = initializer.compressionOptions; this.opsFactory = initializer.opsFactory; this.readTimeout = initializer.readTimeout; this.requestTimeout = initializer.requestTimeout; @@ -1261,7 +1278,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, opsFactory, readTimeout, requestTimeout, + listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, compressionOptions, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders()); return; } @@ -1269,7 +1286,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, readTimeout, requestTimeout, uriTagValue); + mapHandle, maxKeepAliveRequests, methodTagValue, metricsRecorder, minCompressionSize, compressionOptions, 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, @@ -1302,6 +1319,7 @@ static final class HttpServerChannelInitializer implements ChannelPipelineConfig final Function methodTagValue; final ChannelMetricsRecorder metricsRecorder; final int minCompressionSize; + final HttpCompressionOptionsSpec compressionOptions; final ChannelOperations.OnSetup opsFactory; final int protocols; final ProxyProtocolSupportType proxyProtocolSupportType; @@ -1329,6 +1347,7 @@ static final class HttpServerChannelInitializer implements ChannelPipelineConfig this.methodTagValue = config.methodTagValue; this.metricsRecorder = config.metricsRecorderInternal(); this.minCompressionSize = config.minCompressionSize; + this.compressionOptions = config.compressionOptions; this.opsFactory = config.channelOperationsProvider(); this.protocols = config._protocols; this.proxyProtocolSupportType = config.proxyProtocolSupportType; @@ -1383,6 +1402,7 @@ else if ((protocols & h11) == h11) { methodTagValue, metricsRecorder, minCompressionSize, + compressionOptions, readTimeout, requestTimeout, uriTagValue); @@ -1414,6 +1434,7 @@ else if ((protocols & h2) == h2) { methodTagValue, metricsRecorder, minCompressionSize, + compressionOptions, opsFactory, readTimeout, requestTimeout, @@ -1437,6 +1458,7 @@ else if ((protocols & h3) == h3) { methodTagValue, metricsRecorder, minCompressionSize, + compressionOptions, opsFactory, readTimeout, requestTimeout, @@ -1466,6 +1488,7 @@ else if ((protocols & h3) == h3) { methodTagValue, metricsRecorder, minCompressionSize, + compressionOptions, opsFactory, readTimeout, requestTimeout, @@ -1491,6 +1514,7 @@ else if ((protocols & h11) == h11) { methodTagValue, metricsRecorder, minCompressionSize, + compressionOptions, readTimeout, requestTimeout, uriTagValue); @@ -1514,6 +1538,7 @@ else if ((protocols & h2c) == h2c) { methodTagValue, metricsRecorder, minCompressionSize, + compressionOptions, 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 a420862efa..ce30b1c683 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2025 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. @@ -94,6 +94,7 @@ import reactor.netty.http.HttpOperations; import reactor.netty.http.logging.HttpMessageArgProviderFactory; import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; import reactor.netty.http.websocket.WebsocketInbound; import reactor.netty.http.websocket.WebsocketOutbound; import reactor.util.Logger; @@ -138,6 +139,7 @@ class HttpServerOperations extends HttpOperations compressionPredicate; + HttpCompressionOptionsSpec compressionOptions; boolean isWebsocket; Function> paramsResolver; String path; @@ -151,6 +153,7 @@ class HttpServerOperations extends HttpOperations compressionPredicate, + @Nullable HttpCompressionOptionsSpec compressionOptions, ConnectionInfo connectionInfo, ServerCookieDecoder decoder, ServerCookieEncoder encoder, @@ -192,6 +196,7 @@ class HttpServerOperations extends HttpOperations compress; + final HttpCompressionOptionsSpec compressionOptions; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; final HttpServerFormDecoderProvider formDecoderProvider; @@ -112,6 +114,7 @@ final class HttpTrafficHandler extends ChannelDuplexHandler implements Runnable HttpTrafficHandler( @Nullable BiPredicate compress, + @Nullable HttpCompressionOptionsSpec compressionOptions, ServerCookieDecoder decoder, ServerCookieEncoder encoder, HttpServerFormDecoderProvider formDecoderProvider, @@ -128,6 +131,7 @@ final class HttpTrafficHandler extends ChannelDuplexHandler implements Runnable this.formDecoderProvider = formDecoderProvider; this.forwardedHeaderHandler = forwardedHeaderHandler; this.compress = compress; + this.compressionOptions = compressionOptions; this.cookieEncoder = encoder; this.cookieDecoder = decoder; this.httpMessageLogFactory = httpMessageLogFactory; @@ -243,6 +247,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { listener, request, compress, + compressionOptions, connectionInfo, cookieDecoder, cookieEncoder, @@ -595,6 +600,7 @@ public void run() { listener, nextRequest, compress, + compressionOptions, 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 ea52b3e054..bc3f50b5bc 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2025 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. @@ -28,6 +28,8 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; +import reactor.netty.http.server.compression.HttpCompressionOptionsSpec; +import reactor.util.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -42,10 +44,24 @@ final class SimpleCompressionHandler extends HttpContentCompressor { boolean decoded; HttpRequest request; - SimpleCompressionHandler() { + private SimpleCompressionHandler() { super((CompressionOptions[]) null); } + private SimpleCompressionHandler(CompressionOptions... options) { + super(options); + } + + static SimpleCompressionHandler create(@Nullable HttpCompressionOptionsSpec compressionOptions) { + if (compressionOptions == null) { + return new SimpleCompressionHandler(); + } + + return new SimpleCompressionHandler( + compressionOptions.adapt() + ); + } + @Override protected void decode(ChannelHandlerContext ctx, HttpRequest msg, List out) throws Exception { decoded = true; diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/BrotliOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/BrotliOption.java new file mode 100644 index 0000000000..5a36de8b09 --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/BrotliOption.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 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.compression; + +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; + +/** + * Brotli compression option configuration. + * + * @author raccoonback + */ +final class BrotliOption implements HttpCompressionOption { + + CompressionOptions adapt() { + return StandardCompressionOptions.brotli(); + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java new file mode 100644 index 0000000000..3ceb3a0abd --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025 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.compression; + +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; +import io.netty.util.internal.ObjectUtil; + +/** + * Deflate compression option configuration. + * + * @author raccoonback + */ +public final class DeflateOption implements HttpCompressionOption { + + private final int compressionLevel; + private final int windowBits; + private final int memoryLevel; + + private DeflateOption(Build build) { + this.compressionLevel = build.compressionLevel; + this.windowBits = build.windowBits; + this.memoryLevel = build.memoryLevel; + } + + static DeflateOption provideDefault() { + return builder().build(); + } + + CompressionOptions adapt() { + return StandardCompressionOptions.deflate( + compressionLevel, + windowBits, + memoryLevel + ); + } + + /** + * Creates a builder for {@link DeflateOption}. + * + * @return a new {@link DeflateOption.Builder} + */ + public static Builder builder() { + return new DeflateOption.Build(); + } + + public interface Builder { + + /** + * Build a new {@link DeflateOption}. + * + * @return a new {@link DeflateOption} + */ + DeflateOption build(); + + /** + * Sets the deflate compression level. + * + * @return a new {@link DeflateOption.Builder} + */ + Builder compressionLevel(int compressionLevel); + + /** + * Sets the deflate window bits. + * + * @return a new {@link DeflateOption.Builder} + */ + Builder windowBits(int windowBits); + + /** + * Sets the deflate memory level. + * + * @return a new {@link DeflateOption.Builder} + */ + Builder memoryLevel(int memoryLevel); + } + + private static final class Build implements Builder { + + private int compressionLevel = 6; + private int windowBits = 12; + private int memoryLevel = 8; + + @Override + public DeflateOption build() { + return new DeflateOption(this); + } + + @Override + public Builder compressionLevel(int compressionLevel) { + ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel"); + this.compressionLevel = compressionLevel; + return this; + } + + @Override + public Builder windowBits(int windowBits) { + ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits"); + this.windowBits = windowBits; + return this; + } + + @Override + public Builder memoryLevel(int memoryLevel) { + ObjectUtil.checkInRange(memoryLevel, 1, 9, "memoryLevel"); + this.memoryLevel = memoryLevel; + return this; + } + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java new file mode 100644 index 0000000000..71fedb8215 --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025 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.compression; + +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; +import io.netty.util.internal.ObjectUtil; + +/** + * GZIP compression option configuration. + * + * @author raccoonback + */ +public final class GzipOption implements HttpCompressionOption { + + private final int compressionLevel; + private final int windowBits; + private final int memoryLevel; + + private GzipOption(Build build) { + this.compressionLevel = build.compressionLevel; + this.windowBits = build.windowBits; + this.memoryLevel = build.memoryLevel; + } + + static GzipOption provideDefault() { + return builder().build(); + } + + CompressionOptions adapt() { + return StandardCompressionOptions.gzip( + compressionLevel, + windowBits, + memoryLevel + ); + } + + /** + * Creates a builder for {@link GzipOption}. + * + * @return a new {@link GzipOption.Builder} + */ + public static Builder builder() { + return new Build(); + } + + public interface Builder { + + /** + * Build a new {@link GzipOption}. + * + * @return a new {@link GzipOption} + */ + GzipOption build(); + + /** + * Sets the gzip compression level. + * + * @return a new {@link GzipOption.Builder} + */ + Builder compressionLevel(int compressionLevel); + + /** + * Sets the gzip window bits. + * + * @return a new {@link GzipOption.Builder} + */ + Builder windowBits(int windowBits); + + /** + * Sets the gzip memory level. + * + * @return a new {@link GzipOption.Builder} + */ + Builder memoryLevel(int memoryLevel); + } + + private static final class Build implements Builder { + + private int compressionLevel = 6; + private int windowBits = 12; + private int memoryLevel = 8; + + @Override + public GzipOption build() { + return new GzipOption(this); + } + + @Override + public Builder compressionLevel(int compressionLevel) { + ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel"); + this.compressionLevel = compressionLevel; + return this; + } + + @Override + public Builder windowBits(int windowBits) { + ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits"); + this.windowBits = windowBits; + return this; + } + + @Override + public Builder memoryLevel(int memoryLevel) { + ObjectUtil.checkInRange(memoryLevel, 1, 9, "memoryLevel"); + this.memoryLevel = memoryLevel; + return this; + } + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/HttpCompressionOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/HttpCompressionOption.java new file mode 100644 index 0000000000..7eed9f06bb --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/HttpCompressionOption.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 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.compression; + +/** + * HTTP compression option configuration. + * + * @author raccoonback + */ +public interface HttpCompressionOption { +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/HttpCompressionOptionsSpec.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/HttpCompressionOptionsSpec.java new file mode 100644 index 0000000000..301fda3b61 --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/HttpCompressionOptionsSpec.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025 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.compression; + +import io.netty.handler.codec.compression.Brotli; +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.Zstd; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * HTTP Compression configuration for the SimpleCompressionHandler. + * + * @author raccoonback + */ +public final class HttpCompressionOptionsSpec { + + private GzipOption gzip; + private DeflateOption deflate; + private SnappyOption snappy; + private BrotliOption brotli; + private ZstdOption zstd; + + private HttpCompressionOptionsSpec() { + gzip = GzipOption.provideDefault(); + deflate = DeflateOption.provideDefault(); + snappy = new SnappyOption(); + + if (Brotli.isAvailable()) { + brotli = new BrotliOption(); + } + + if (Zstd.isAvailable()) { + zstd = ZstdOption.provideDefault(); + } + } + + public HttpCompressionOptionsSpec(HttpCompressionOption... compressionOptions) { + this(); + Arrays.stream(compressionOptions) + .forEach(this::initializeOption); + } + + private void initializeOption(HttpCompressionOption option) { + if (option instanceof GzipOption) { + this.gzip = (GzipOption) option; + } + else if (option instanceof DeflateOption) { + this.deflate = (DeflateOption) option; + } + else if (option instanceof SnappyOption) { + this.snappy = (SnappyOption) option; + } + else if (Brotli.isAvailable() && option instanceof BrotliOption) { + this.brotli = (BrotliOption) option; + } + else if (Zstd.isAvailable() && option instanceof ZstdOption) { + this.zstd = (ZstdOption) option; + } + } + + public CompressionOptions[] adapt() { + List options = new ArrayList<>( + Arrays.asList( + gzip.adapt(), + deflate.adapt(), + snappy.adapt() + ) + ); + + if (brotli != null) { + options.add(brotli.adapt()); + } + + if (zstd != null) { + options.add(zstd.adapt()); + } + + return options.toArray(new CompressionOptions[0]); + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/SnappyOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/SnappyOption.java new file mode 100644 index 0000000000..14a21a7cd3 --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/SnappyOption.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 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.compression; + +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; + +/** + * Snappy compression option configuration. + * + * @author raccoonback + */ +final class SnappyOption implements HttpCompressionOption { + + CompressionOptions adapt() { + return StandardCompressionOptions.snappy(); + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java new file mode 100644 index 0000000000..70f0484db2 --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 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.compression; + +import io.netty.handler.codec.compression.CompressionOptions; +import io.netty.handler.codec.compression.StandardCompressionOptions; +import io.netty.handler.codec.compression.Zstd; +import io.netty.util.internal.ObjectUtil; + +/** + * ZSTD compression option configuration. + * + * @author raccoonback + */ +public final class ZstdOption implements HttpCompressionOption { + + private final int compressionLevel; + private final int blockSize; + private final int maxEncodeSize; + + private ZstdOption(Build build) { + this.compressionLevel = build.compressionLevel; + this.blockSize = build.blockSize; + this.maxEncodeSize = build.maxEncodeSize; + } + + static ZstdOption provideDefault() { + return builder().build(); + } + + CompressionOptions adapt() { + return StandardCompressionOptions.zstd( + compressionLevel, + blockSize, + maxEncodeSize + ); + } + + /** + * Creates a builder for {@link ZstdOption}. + * + * @return a new {@link ZstdOption.Builder} + */ + public static Builder builder() { + if (!Zstd.isAvailable()) { + throw new IllegalStateException("zstd is not available", Zstd.cause()); + } + + return new ZstdOption.Build(); + } + + public interface Builder { + + /** + * Build a new {@link ZstdOption}. + * + * @return a new {@link ZstdOption} + */ + ZstdOption build(); + + /** + * Sets the zstd compression level. + * + * @return a new {@link ZstdOption.Builder} + */ + Builder compressionLevel(int compressionLevel); + + /** + * Sets the zstd block size. + * + * @return a new {@link ZstdOption.Builder} + */ + Builder blockSize(int blockSize); + + /** + * Sets the zstd memory level. + * + * @return a new {@link ZstdOption.Builder} + */ + Builder maxEncodeSize(int maxEncodeSize); + } + + private static final class Build implements Builder { + + private int compressionLevel = 3; + private int blockSize = 1 << 16; // 64KB + private int maxEncodeSize = 1 << (compressionLevel + 7 + 0x0F); // 32MB + + @Override + public ZstdOption build() { + return new ZstdOption(this); + } + + @Override + public Builder compressionLevel(int compressionLevel) { + ObjectUtil.checkInRange(compressionLevel, -(1 << 17), 22, "compressionLevel"); + this.compressionLevel = compressionLevel; + return this; + } + + @Override + public Builder blockSize(int blockSize) { + ObjectUtil.checkPositive(blockSize, "blockSize"); + this.blockSize = blockSize; + return this; + } + + @Override + public Builder maxEncodeSize(int maxEncodeSize) { + ObjectUtil.checkPositive(maxEncodeSize, "maxEncodeSize"); + this.maxEncodeSize = maxEncodeSize; + return this; + } + } +} 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 89bbe61c96..11737da3b5 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2025 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. @@ -35,7 +35,10 @@ import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.compression.Brotli; +import io.netty.handler.codec.compression.ZlibCodecFactory; +import io.netty.handler.codec.compression.ZlibWrapper; import io.netty.handler.codec.compression.Zstd; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; @@ -56,6 +59,8 @@ import reactor.netty.http.client.HttpClient; import reactor.netty.http.server.HttpServer; import reactor.netty.http.server.HttpServerResponse; +import reactor.netty.http.server.compression.GzipOption; +import reactor.netty.http.server.compression.ZstdOption; import reactor.test.StepVerifier; import reactor.util.function.Tuple2; @@ -718,4 +723,125 @@ void serverCompressionEnabledResponseCompressionDisabled(HttpServer server, Http .expectComplete() .verify(Duration.ofSeconds(10)); } + + @ParameterizedCompressionTest + void serverCompressionWithCompressionLevelSettings(HttpServer server, HttpClient client) { + disposableServer = + server.compress(true) + .compressOptions( + GzipOption.builder() + .compressionLevel(4) + .windowBits(15) + .memoryLevel(8) + .build() + ) + .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)); + + EmbeddedChannel embeddedChannel = new EmbeddedChannel( + ZlibCodecFactory.newZlibEncoder( + ZlibWrapper.GZIP, + 4, + 15, + 8 + ) + ); + + ByteBuf byteBuf = Unpooled.directBuffer(32); + byteBuf.writeBytes("reply".getBytes(Charset.defaultCharset())); + + embeddedChannel.writeOutbound(byteBuf); + ByteBuf encodedByteBuf = embeddedChannel.readOutbound(); + + byte[] result = new byte[encodedByteBuf.readableBytes()]; + encodedByteBuf.getBytes(encodedByteBuf.readerIndex(), result); + + 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( + GzipOption.builder() + .compressionLevel(4) + .windowBits(15) + .memoryLevel(8) + .build() + ) + .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( + ZstdOption.builder() + .compressionLevel(12) + .blockSize(65536) + .maxEncodeSize(65536) + .build() + ) + .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 7edbaaed3a..d3ee56ad04 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2025 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. @@ -2214,6 +2214,7 @@ private void doTestStatus(HttpResponseStatus status) { ConnectionObserver.emptyListener(), new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"), null, + null, new ConnectionInfo(localSocketAddress, DEFAULT_HOST_NAME, DEFAULT_HTTP_PORT, remoteSocketAddress, "http", true), ServerCookieDecoder.STRICT, ServerCookieEncoder.STRICT, @@ -3272,6 +3273,7 @@ private void doTestIsFormUrlencoded(String headerValue, boolean expectation) { ConnectionObserver.emptyListener(), request, null, + null, new ConnectionInfo(localSocketAddress, DEFAULT_HOST_NAME, DEFAULT_HTTP_PORT, remoteSocketAddress, "http", true), ServerCookieDecoder.STRICT, ServerCookieEncoder.STRICT,