Fix support for optional encoders errors in HttpContentCompressor (#11582)

Motivation:

- Fix `HttpContentCompressor` errors due to missing optional compressor libraries such as Brotli and Zstd at runtime.
- Improve support for optional encoders by only considering the `CompressionOptions` provided to the constructor and ignoring those for which the encoder is unavailable.

Modification:

The `HttpContentCompressor` constructor now only creates encoder factories for the CompressionOptions passed to the constructor when the encoder is available which must be checked for Brotli and Zstd. In case of Brotli, it is not possible to create BrotliOptions if brotly4j is not available so there's actually nothing to check. In case of Zstd, I had to create class `io.netty.handler.codec.compression.Zstd` similar to `io.netty.handler.codec.compression.Brotli` which is used to check that zstd-jni is availabie at runtime.

The `determineEncoding()` method had to change as well in order to ignore encodings for which there's no `CompressionEncoderFactory` instance.

When the HttpContentCompressor is created using deprecated constructor (ie. with no CompressionOptions), we consider all available encoders.

Result:

Fixes #11581.
This commit is contained in:
Jeremy Kuhn 2021-08-19 08:40:25 +02:00 committed by Norman Maurer
parent b0e28e3740
commit 009497f5f9
4 changed files with 135 additions and 43 deletions

View File

@ -91,6 +91,11 @@
<artifactId>native-windows-x86_64</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.luben</groupId>
<artifactId>zstd-jni</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -15,27 +15,28 @@
*/
package io.netty.handler.codec.http;
import java.util.HashMap;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.compression.ZlibEncoder;
import io.netty.handler.codec.compression.Brotli;
import io.netty.handler.codec.compression.BrotliEncoder;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibWrapper;
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.StandardCompressionOptions;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibEncoder;
import io.netty.handler.codec.compression.ZlibWrapper;
import io.netty.handler.codec.compression.Zstd;
import io.netty.handler.codec.compression.ZstdEncoder;
import io.netty.handler.codec.compression.ZstdOptions;
import io.netty.util.internal.ObjectUtil;
import java.util.HashMap;
import java.util.Map;
/**
* Compresses an {@link HttpMessage} and an {@link HttpContent} in {@code gzip} or
* {@code deflate} encoding while respecting the {@code "Accept-Encoding"} header.
@ -138,7 +139,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
this.deflateOptions = null;
this.zstdOptions = null;
this.factories = null;
supportsCompressionOptions = false;
this.supportsCompressionOptions = false;
}
/**
@ -170,51 +171,50 @@ public class HttpContentCompressor extends HttpContentEncoder {
DeflateOptions deflateOptions = null;
ZstdOptions zstdOptions = null;
if (compressionOptions == null || compressionOptions.length == 0) {
brotliOptions = StandardCompressionOptions.brotli();
brotliOptions = Brotli.isAvailable() ? StandardCompressionOptions.brotli() : null;
gzipOptions = StandardCompressionOptions.gzip();
deflateOptions = StandardCompressionOptions.deflate();
zstdOptions = StandardCompressionOptions.zstd();
zstdOptions = Zstd.isAvailable() ? StandardCompressionOptions.zstd() : null;
} else {
ObjectUtil.deepCheckNotNull("compressionOptions", compressionOptions);
for (CompressionOptions compressionOption : compressionOptions) {
if (compressionOption instanceof BrotliOptions) {
// if we have BrotliOptions, it means Brotli is available
brotliOptions = (BrotliOptions) compressionOption;
} else if (compressionOption instanceof GzipOptions) {
gzipOptions = (GzipOptions) compressionOption;
} else if (compressionOption instanceof DeflateOptions) {
deflateOptions = (DeflateOptions) compressionOption;
} else if (compressionOption instanceof ZstdOptions) {
zstdOptions = (ZstdOptions) compressionOption;
// zstd might not be available
zstdOptions = Zstd.isAvailable() ? (ZstdOptions) compressionOption : null;
} else {
throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
": " + compressionOption);
}
}
if (brotliOptions == null) {
brotliOptions = StandardCompressionOptions.brotli();
}
if (gzipOptions == null) {
gzipOptions = StandardCompressionOptions.gzip();
}
if (deflateOptions == null) {
deflateOptions = StandardCompressionOptions.deflate();
}
if (zstdOptions == null) {
zstdOptions = StandardCompressionOptions.zstd();
}
}
this.brotliOptions = brotliOptions;
this.gzipOptions = gzipOptions;
this.deflateOptions = deflateOptions;
this.brotliOptions = brotliOptions;
this.zstdOptions = zstdOptions;
this.factories = new HashMap<String, CompressionEncoderFactory>() {
{
put("gzip", new GzipEncoderFactory());
put("deflate", new DeflateEncoderFactory());
put("br", new BrEncoderFactory());
put("zstd", new ZstdEncoderFactory());
}
};
this.factories = new HashMap<String, CompressionEncoderFactory>();
if (this.gzipOptions != null) {
this.factories.put("gzip", new GzipEncoderFactory());
}
if (this.deflateOptions != null) {
this.factories.put("deflate", new DeflateEncoderFactory());
}
if (this.brotliOptions != null) {
this.factories.put("br", new BrEncoderFactory());
}
if (this.zstdOptions != null) {
this.factories.put("zstd", new ZstdEncoderFactory());
}
this.compressionLevel = -1;
this.windowBits = -1;
this.memLevel = -1;
@ -314,27 +314,27 @@ public class HttpContentCompressor extends HttpContentEncoder {
}
}
if (brQ > 0.0f || zstdQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
if (brQ != -1.0f && brQ >= zstdQ) {
return Brotli.isAvailable() ? "br" : null;
} else if (zstdQ != -1.0f && zstdQ >= gzipQ) {
if (brQ != -1.0f && brQ >= zstdQ && this.brotliOptions != null) {
return "br";
} else if (zstdQ != -1.0f && zstdQ >= gzipQ && this.zstdOptions != null) {
return "zstd";
} else if (gzipQ != -1.0f && gzipQ >= deflateQ) {
} else if (gzipQ != -1.0f && gzipQ >= deflateQ && this.gzipOptions != null) {
return "gzip";
} else if (deflateQ != -1.0f) {
} else if (deflateQ != -1.0f && this.deflateOptions != null) {
return "deflate";
}
}
if (starQ > 0.0f) {
if (brQ == -1.0f) {
return Brotli.isAvailable() ? "br" : null;
if (brQ == -1.0f && this.brotliOptions != null) {
return "br";
}
if (zstdQ == -1.0f) {
if (zstdQ == -1.0f && this.zstdOptions != null) {
return "zstd";
}
if (gzipQ == -1.0f) {
if (gzipQ == -1.0f && this.gzipOptions != null) {
return "gzip";
}
if (deflateQ == -1.0f) {
if (deflateQ == -1.0f && this.deflateOptions != null) {
return "deflate";
}
}

View File

@ -16,6 +16,8 @@
package io.netty.handler.codec.http;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.compression.StandardCompressionOptions;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.instanceOf;
@ -30,7 +32,12 @@ class HttpContentCompressorOptionsTest {
@Test
void testGetBrTargetContentEncoding() {
HttpContentCompressor compressor = new HttpContentCompressor();
HttpContentCompressor compressor = new HttpContentCompressor(
StandardCompressionOptions.gzip(),
StandardCompressionOptions.deflate(),
StandardCompressionOptions.brotli(),
StandardCompressionOptions.zstd()
);
String[] tests = {
// Accept-Encoding -> Content-Encoding
@ -52,7 +59,12 @@ class HttpContentCompressorOptionsTest {
@Test
void testGetZstdTargetContentEncoding() {
HttpContentCompressor compressor = new HttpContentCompressor();
HttpContentCompressor compressor = new HttpContentCompressor(
StandardCompressionOptions.gzip(),
StandardCompressionOptions.deflate(),
StandardCompressionOptions.brotli(),
StandardCompressionOptions.zstd()
);
String[] tests = {
// Accept-Encoding -> Content-Encoding

View File

@ -0,0 +1,75 @@
/*
* Copyright 2021 The Netty Project
*
* The Netty Project licenses this file to you 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 io.netty.handler.codec.compression;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
public final class Zstd {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Zstd.class);
private static final Throwable cause;
static {
Throwable t = null;
try {
Class.forName("com.github.luben.zstd.Zstd", false,
PlatformDependent.getClassLoader(Zstd.class));
} catch (ClassNotFoundException e) {
t = e;
logger.debug(
"zstd-jni not in the classpath; Zstd support will be unavailable.");
} catch (Throwable e) {
t = e;
logger.debug("Failed to load zstd-jni; Zstd support will be unavailable.", t);
}
cause = t;
}
/**
*
* @return true when zstd-jni is in the classpath
* and native library is available on this platform and could be loaded
*/
public static boolean isAvailable() {
return cause == null;
}
/**
* Throws when zstd support is missing from the classpath or is unavailable on this platform
* @throws Throwable a ClassNotFoundException if zstd-jni is missing
* or a ExceptionInInitializerError if zstd native lib can't be loaded
*/
public static void ensureAvailability() throws Throwable {
if (cause != null) {
throw cause;
}
}
/**
* Returns {@link Throwable} of unavailability cause
*/
public static Throwable cause() {
return cause;
}
private Zstd() {
}
}