You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2025/06/19 16:36:45 UTC

(httpcomponents-client) branch master updated: HTTPCLIENT-1843 Plug Commons-Compress into HttpClient’s automatic (#651)

This is an automated email from the ASF dual-hosted git repository.

abernal pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git


The following commit(s) were added to refs/heads/master by this push:
     new d1eb5b8b0 HTTPCLIENT-1843  Plug Commons-Compress into HttpClient’s automatic (#651)
d1eb5b8b0 is described below

commit d1eb5b8b09b32bfe72b364edce378eb21a416167
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Thu Jun 19 18:36:40 2025 +0200

    HTTPCLIENT-1843  Plug Commons-Compress into HttpClient’s automatic (#651)
    
    content-decoding (optional)
    
    * New ContentDecoderRegistry discovers extra codecs (br, zstd, xz, lz4, …)
      via Commons-Compress when that jar is on the class-path; otherwise falls
      back to the built-ins (gzip, deflate) only.
    * No hard dependency added—projects that need the extra algorithms just add
      `commons-compress` (and helper jars like google-brotli, zstd-jni, xz-java)
      to their pom and HttpClient uses them automatically.
---
 .../hc/client5/testing/sync/TestRedirects.java     |   4 +-
 httpclient5/pom.xml                                |   5 +
 .../http/entity/BrotliInputStreamFactory.java      |  11 ++
 .../http/entity/DeflateInputStreamFactory.java     |  11 ++
 .../http/entity/GZIPInputStreamFactory.java        |  11 ++
 .../hc/client5/http/entity/InputStreamFactory.java |  17 +++
 .../compress/CommonsCompressDecoderFactory.java    | 117 +++++++++++++++++
 .../http/entity/compress/ContentCoding.java        | 136 +++++++++++++++++++
 .../entity/compress/ContentDecoderRegistry.java    | 144 +++++++++++++++++++++
 .../http/impl/classic/ContentCompressionExec.java  |  58 +++++----
 pom.xml                                            |   7 +
 11 files changed, 495 insertions(+), 26 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
index 8b67437ca..25c9e7f41 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java
@@ -646,8 +646,8 @@ public void handle(final ClassicHttpRequest request,
         Assertions.assertEquals(new URIBuilder().setHttpHost(target).setPath("/random/100").build(),
                 reqWrapper.getUri());
 
-        assertThat(values.poll(), CoreMatchers.equalTo("gzip, x-gzip, deflate"));
-        assertThat(values.poll(), CoreMatchers.equalTo("gzip, x-gzip, deflate"));
+        assertThat(values.poll(), CoreMatchers.equalTo("gzip, deflate, lz4-framed, lz4-block, bzip2, pack200, deflate64, x-gzip"));
+        assertThat(values.poll(), CoreMatchers.equalTo("gzip, deflate, lz4-framed, lz4-block, bzip2, pack200, deflate64, x-gzip"));
         assertThat(values.poll(), CoreMatchers.nullValue());
     }
 
diff --git a/httpclient5/pom.xml b/httpclient5/pom.xml
index 47777876b..b66f028f0 100644
--- a/httpclient5/pom.xml
+++ b/httpclient5/pom.xml
@@ -108,6 +108,11 @@
       <artifactId>commons-io</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-compress</artifactId>
+      <optional>true</optional>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java
index b5eac4fec..68abf0aed 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java
@@ -41,6 +41,17 @@
 @Contract(threading = ThreadingBehavior.STATELESS)
 public class BrotliInputStreamFactory implements InputStreamFactory {
 
+    /**
+     * Canonical token for the deflate content-coding.
+     * @since 5.6
+     */
+    public static final String ENCODING = "br";
+
+    @Override
+    public String getContentEncoding() {
+        return ENCODING;
+    }
+
     /**
      * Default instance of {@link BrotliInputStreamFactory}.
      */
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java
index cfd113a76..ab1a285ab 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java
@@ -41,6 +41,17 @@
 @Contract(threading = ThreadingBehavior.STATELESS)
 public class DeflateInputStreamFactory implements InputStreamFactory {
 
+    /**
+     * Canonical token for the deflate content-coding.
+     * @since 5.6
+     */
+    public static final String ENCODING = "deflate";
+
+    @Override
+    public String getContentEncoding() {
+        return ENCODING;
+    }
+
     /**
      * Default instance of {@link DeflateInputStreamFactory}.
      */
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java
index a03e20dd9..f59712afe 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java
@@ -42,6 +42,17 @@
 @Contract(threading = ThreadingBehavior.STATELESS)
 public class GZIPInputStreamFactory implements InputStreamFactory {
 
+    /**
+     * Canonical token for the gzip content-coding.
+     * @since 5.6
+     */
+    public static final String ENCODING = "gzip";
+
+    @Override
+    public String getContentEncoding() {
+        return ENCODING;
+    }
+
     /**
      * Default instance of {@link GZIPInputStreamFactory}.
      */
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java
index a6689435e..b2664648e 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java
@@ -38,4 +38,21 @@ public interface InputStreamFactory {
 
     InputStream create(InputStream inputStream) throws IOException;
 
+    /**
+     * Returns the canonical {@code Content-Encoding} token handled by this
+     * factory (for example {@code "gzip"}, {@code "deflate"}, {@code "br"}).
+     * <p>
+     * Implementations that do <strong>not</strong> represent a HTTP
+     * content-decoder should simply inherit the default implementation,
+     * which returns an empty string.
+     *
+     * @return the lower-case encoding token, or an empty string when the
+     *         factory is not intended for HTTP content-decoding
+     *
+     * @since 5.6
+     */
+    default String getContentEncoding() {
+        return "";
+    }
+
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java
new file mode 100644
index 000000000..412ba414b
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java
@@ -0,0 +1,117 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
+ *
+ *   http://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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.entity.compress;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.hc.client5.http.entity.InputStreamFactory;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+/**
+ * A factory for creating InputStream instances, utilizing Apache Commons Compress.
+ * This class is compiled with Commons Compress as an optional dependency, loading
+ * only when the library is present at runtime, avoiding mandatory inclusion in
+ * downstream builds.
+ * <p>
+ * <p>
+ * Some encodings require native helper JARs; runtime availability is checked
+ * using a lightweight Class.forName probe to register codecs only when helpers
+ * are present.
+ *
+ * @since 5.6
+ */
+@Internal
+@Contract(threading = ThreadingBehavior.STATELESS)
+final class CommonsCompressDecoderFactory implements InputStreamFactory {
+
+
+    /**
+     * Map of codings that need extra JARs → the fully‐qualified class we test for
+     */
+    private static final Map<ContentCoding, String> REQUIRED_CLASS_NAME;
+
+    static {
+        final Map<ContentCoding, String> m = new EnumMap<>(ContentCoding.class);
+        m.put(ContentCoding.BROTLI, "org.brotli.dec.BrotliInputStream");
+        m.put(ContentCoding.ZSTD, "com.github.luben.zstd.ZstdInputStream");
+        m.put(ContentCoding.XZ, "org.tukaani.xz.XZInputStream");
+        m.put(ContentCoding.LZMA, "org.tukaani.xz.XZInputStream");
+        REQUIRED_CLASS_NAME = Collections.unmodifiableMap(m);
+    }
+
+    private final String encoding;
+
+    CommonsCompressDecoderFactory(final String encoding) {
+        this.encoding = encoding.toLowerCase(Locale.ROOT);
+    }
+
+    @Override
+    public String getContentEncoding() {
+        return encoding;
+    }
+
+    @Override
+    public InputStream create(final InputStream source) throws IOException {
+        try {
+            return new CompressorStreamFactory()
+                    .createCompressorInputStream(encoding, source);
+        } catch (final CompressorException | LinkageError ex) {
+            throw new IOException(
+                    "Unable to decode Content-Encoding '" + encoding + '\'', ex);
+        }
+    }
+
+
+    static boolean runtimeAvailable(final String token) {
+        final ContentCoding coding = ContentCoding.fromToken(token);
+        if (coding == null) {
+            return true;
+        }
+        final String helper = REQUIRED_CLASS_NAME.get(coding);
+        if (helper == null) {
+            // no extra JAR needed
+            return true;
+        }
+        try {
+            Class.forName(helper, false,
+                    CommonsCompressDecoderFactory.class.getClassLoader());
+            return true;
+        } catch (final ClassNotFoundException | LinkageError ex) {
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCoding.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCoding.java
new file mode 100644
index 000000000..3841a4ed0
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCoding.java
@@ -0,0 +1,136 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
+ *
+ *   http://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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.entity.compress;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Enumeration of the canonical IANA content-coding tokens supported by HttpClient for
+ * HTTP request and response bodies.
+ * <p>
+ * Each constant corresponds to the standard token used in the {@code Content-Encoding}
+ * and {@code Accept-Encoding} headers.  Some codings (e.g. Brotli, Zstandard, XZ/LZMA)
+ * may require additional helper libraries at runtime.
+ *
+ * @since 5.6
+ */
+public enum ContentCoding {
+
+    /**
+     * GZIP compression format.
+     */
+    GZIP("gzip"),
+    /**
+     * "deflate" compression format (zlib or raw).
+     */
+    DEFLATE("deflate"),
+    /**
+     * Legacy alias for GZIP.
+     */
+    X_GZIP("x-gzip"),
+
+    // Optional codecs requiring Commons-Compress or native helpers
+    /**
+     * Brotli compression format.
+     */
+    BROTLI("br"),
+    /**
+     * Zstandard compression format.
+     */
+    ZSTD("zstd"),
+    /**
+     * XZ compression format.
+     */
+    XZ("xz"),
+    /**
+     * LZMA compression format.
+     */
+    LZMA("lzma"),
+    /**
+     * Framed LZ4 compression format.
+     */
+    LZ4_FRAMED("lz4-framed"),
+    /**
+     * Block LZ4 compression format.
+     */
+    LZ4_BLOCK("lz4-block"),
+    /**
+     * BZIP2 compression format.
+     */
+    BZIP2("bzip2"),
+    /**
+     * Pack200 compression format.
+     */
+    PACK200("pack200"),
+    /**
+     * Deflate64 compression format.
+     */
+    DEFLATE64("deflate64");
+
+    private static final Map<String, ContentCoding> TOKEN_LOOKUP;
+    static {
+        final Map<String, ContentCoding> map = new HashMap<>(values().length, 1f);
+        for (final ContentCoding contentCoding : values()) {
+            map.put(contentCoding.token, contentCoding);
+        }
+        TOKEN_LOOKUP = Collections.unmodifiableMap(map);
+    }
+
+    private final String token;
+
+    ContentCoding(final String token) {
+        this.token = token;
+    }
+
+    /**
+     * Returns the standard IANA token string for this content-coding.
+     *
+     * @return the lowercase token used in HTTP headers
+     */
+    public String token() {
+        return token;
+    }
+
+    /**
+     * Lookup an enum by its token (case‐insensitive), or {@code null} if none matches.
+     * <p>
+     * This method is backed by a static, pre‐populated map so the lookup is O(1)
+     * instead of O(n).</p>
+     *
+     * @param token the content‐coding token to look up
+     * @return the matching enum constant, or {@code null} if none
+     */
+    public static ContentCoding fromToken(final String token) {
+        return TOKEN_LOOKUP.get(
+                token == null ? null : token.toLowerCase(Locale.ROOT)
+        );
+    }
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentDecoderRegistry.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentDecoderRegistry.java
new file mode 100644
index 000000000..a019f705e
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentDecoderRegistry.java
@@ -0,0 +1,144 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
+ *
+ *   http://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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.entity.compress;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.hc.client5.http.entity.BrotliDecompressingEntity;
+import org.apache.hc.client5.http.entity.BrotliInputStreamFactory;
+import org.apache.hc.client5.http.entity.DeflateInputStreamFactory;
+import org.apache.hc.client5.http.entity.GZIPInputStreamFactory;
+import org.apache.hc.client5.http.entity.InputStreamFactory;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.Internal;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+/**
+ * Immutable run-time catalogue of {@link InputStreamFactory} instances
+ * capable of <em>decoding</em> HTTP entity bodies.
+ *
+ * <p>The map is populated once during class initialisation:</p>
+ * <ol>
+ *     <li>Built-ins: {@code gzip} and {@code deflate} are always present.</li>
+ *     <li>If Commons-Compress is on the class-path we register a configurable
+ *         list of codecs (br, zstd, xz, …) via
+ *         {@link CommonsCompressDecoderFactory} – guarded by a cheap
+ *         presence check.</li>
+ *     <li>If Commons was absent or could not supply <code>br</code>,
+ *         we fall back to the pure native singleton
+ *         {@link BrotliInputStreamFactory} (when the <code>org.brotli</code>
+ *         decoder JAR is available).</li>
+ * </ol>
+ *
+ * <p>The resulting {@code Map} is wrapped in
+ * {@link Collections#unmodifiableMap(Map)} and published through
+ * {@link #getRegistry()} for safe, lock-free concurrent reads.</p>
+ *
+ * @since 5.6
+ */
+@Internal
+@Contract(threading = ThreadingBehavior.STATELESS)
+public final class ContentDecoderRegistry {
+
+    private static final String CCSF =
+            "org.apache.commons.compress.compressors.CompressorStreamFactory";
+
+
+    private static final Map<ContentCoding, InputStreamFactory> REGISTRY = buildRegistry();
+
+
+    /**
+     * Returns the unmodifiable codec map (key = canonical token, value = factory).
+     */
+    public static Map<ContentCoding, InputStreamFactory> getRegistry() {
+        return REGISTRY;
+    }
+
+
+    private static Map<ContentCoding, InputStreamFactory> buildRegistry() {
+        final LinkedHashMap<ContentCoding, InputStreamFactory> m = new LinkedHashMap<>();
+
+        // 1. Built-ins
+        register(m, ContentCoding.GZIP, new GZIPInputStreamFactory());
+        register(m, ContentCoding.DEFLATE, new DeflateInputStreamFactory());
+
+        // 2. Commons-Compress (optional)
+        if (commonsCompressPresent()) {
+            for (final ContentCoding coding : Arrays.asList(
+                    ContentCoding.BROTLI,
+                    ContentCoding.ZSTD,
+                    ContentCoding.XZ,
+                    ContentCoding.LZMA,
+                    ContentCoding.LZ4_FRAMED,
+                    ContentCoding.LZ4_BLOCK,
+                    ContentCoding.BZIP2,
+                    ContentCoding.PACK200,
+                    ContentCoding.DEFLATE64)) {
+                addCommons(m, coding);
+            }
+        }
+
+        // 3. Native Brotli fallback if Commons did not register it
+        if (!m.containsKey(ContentCoding.BROTLI)
+                && BrotliDecompressingEntity.isAvailable()) {
+            register(m, ContentCoding.BROTLI, new BrotliInputStreamFactory());
+        }
+
+        return Collections.unmodifiableMap(m);
+    }
+
+    private static void register(final Map<ContentCoding, InputStreamFactory> map,
+                                 final ContentCoding coding,
+                                 final InputStreamFactory factory) {
+        map.put(coding, factory);
+    }
+
+    private static void addCommons(final Map<ContentCoding, InputStreamFactory> map,
+                                   final ContentCoding coding) {
+        if (CommonsCompressDecoderFactory.runtimeAvailable(coding.token())) {
+            register(map, coding, new CommonsCompressDecoderFactory(coding.token()));
+        }
+    }
+
+    private static boolean commonsCompressPresent() {
+        try {
+            Class.forName(
+                    CCSF, false, ContentDecoderRegistry.class.getClassLoader());
+            return true;
+        } catch (final ClassNotFoundException | LinkageError ex) {
+            return false;
+        }
+    }
+
+    private ContentDecoderRegistry() {
+    }
+}
+
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
index ac3cd5a55..4b9d7289d 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java
@@ -29,20 +29,20 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.zip.GZIPInputStream;
 
 import org.apache.hc.client5.http.classic.ExecChain;
 import org.apache.hc.client5.http.classic.ExecChainHandler;
 import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.entity.BrotliDecompressingEntity;
-import org.apache.hc.client5.http.entity.BrotliInputStreamFactory;
 import org.apache.hc.client5.http.entity.DecompressingEntity;
 import org.apache.hc.client5.http.entity.DeflateInputStream;
-import org.apache.hc.client5.http.entity.DeflateInputStreamFactory;
-import org.apache.hc.client5.http.entity.GZIPInputStreamFactory;
 import org.apache.hc.client5.http.entity.InputStreamFactory;
+import org.apache.hc.client5.http.entity.compress.ContentCoding;
+import org.apache.hc.client5.http.entity.compress.ContentDecoderRegistry;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.Internal;
@@ -81,36 +81,47 @@ public final class ContentCompressionExec implements ExecChainHandler {
     private final Lookup<InputStreamFactory> decoderRegistry;
     private final boolean ignoreUnknown;
 
+    private static final Map<ContentCoding, InputStreamFactory> DECODERS = ContentDecoderRegistry.getRegistry();
+
+    /**
+     * Pre-built list of all supported tokens (plus X-GZIP alias) for
+     * the Accept-Encoding header, to avoid reconstructing it every time.
+     */
+    private static final List<String> DEFAULT_ACCEPT_ENCODINGS;
+    static {
+        final List<String> tmp = new ArrayList<>(DECODERS.size() + 1);
+        for (final ContentCoding coding : DECODERS.keySet()) {
+            tmp.add(coding.token());
+        }
+        // add x-gzip alias if gzip is present
+        if (DECODERS.containsKey(ContentCoding.GZIP)) {
+            tmp.add(ContentCoding.X_GZIP.token());
+        }
+        DEFAULT_ACCEPT_ENCODINGS = Collections.unmodifiableList(tmp);
+    }
+
     public ContentCompressionExec(
             final List<String> acceptEncoding,
             final Lookup<InputStreamFactory> decoderRegistry,
             final boolean ignoreUnknown) {
 
-        final boolean brotliSupported = decoderRegistry == null && BrotliDecompressingEntity.isAvailable();
-        if (acceptEncoding != null) {
-            this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptEncoding);
-        } else {
-            final List<String> encodings = new ArrayList<>(4);
-            encodings.add("gzip");
-            encodings.add("x-gzip");
-            encodings.add("deflate");
-            if (brotliSupported) {
-                encodings.add("br");
-            }
-            this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, encodings);
-        }
+        final List<String> encodingsHeader = acceptEncoding != null ? acceptEncoding : DEFAULT_ACCEPT_ENCODINGS;
+
+        this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, encodingsHeader);
+
         if (decoderRegistry != null) {
             this.decoderRegistry = decoderRegistry;
         } else {
-            final RegistryBuilder<InputStreamFactory> builder = RegistryBuilder.<InputStreamFactory>create()
-                .register("gzip", GZIPInputStreamFactory.getInstance())
-                .register("x-gzip", GZIPInputStreamFactory.getInstance())
-                .register("deflate", DeflateInputStreamFactory.getInstance());
-            if (brotliSupported) {
-                builder.register("br", BrotliInputStreamFactory.getInstance());
+            final RegistryBuilder<InputStreamFactory> builder = RegistryBuilder.create();
+            DECODERS.forEach((coding, factory) ->
+                    builder.register(coding.token(), factory));
+            // register the x-gzip alias again
+            if (DECODERS.containsKey(ContentCoding.GZIP)) {
+                builder.register(ContentCoding.X_GZIP.token(), DECODERS.get(ContentCoding.GZIP));
             }
             this.decoderRegistry = builder.build();
         }
+
         this.ignoreUnknown = ignoreUnknown;
     }
 
@@ -176,5 +187,4 @@ public ClassicHttpResponse execute(
         }
         return response;
     }
-
 }
diff --git a/pom.xml b/pom.xml
index 5fd9b3442..7556ea010 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,7 @@
     <junixsocket.version>2.10.1</junixsocket.version>
     <api.comparison.version>5.3</api.comparison.version>
     <hc.animal-sniffer.signature.ignores>javax.net.ssl.SSLEngine,javax.net.ssl.SSLParameters,java.nio.ByteBuffer,java.nio.CharBuffer</hc.animal-sniffer.signature.ignores>
+    <commons.compress.version>1.27.1</commons.compress.version>
   </properties>
 
   <dependencyManagement>
@@ -204,6 +205,12 @@
         <artifactId>commons-io</artifactId>
         <version>2.19.0</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-compress</artifactId>
+        <version>${commons.compress.version}</version>
+      </dependency>
+
     </dependencies>
   </dependencyManagement>