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 2026/05/30 09:46:03 UTC

(httpcomponents-client) branch master updated: HTTPCLIENT-2422: Restore lazy content decompression

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

arturobernalg 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 4b0cbd4bf HTTPCLIENT-2422: Restore lazy content decompression
4b0cbd4bf is described below

commit 4b0cbd4bf94a92b703985630aa42144eed20402c
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Wed May 27 07:07:23 2026 +0200

    HTTPCLIENT-2422: Restore lazy content decompression
    
    Defer construction of the decompression stream until the response
    content is actually read.
---
 .../http/entity/compress/DecompressingEntity.java  | 69 +++++++++++++++++++++-
 .../http/entity/TestDecompressingEntity.java       | 24 ++++++++
 2 files changed, 90 insertions(+), 3 deletions(-)

diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java
index 070663ccc..38b00a3ac 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java
@@ -27,9 +27,11 @@
 
 package org.apache.hc.client5.http.entity.compress;
 
+import java.io.FilterInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
 import org.apache.hc.core5.io.IOFunction;
@@ -48,19 +50,23 @@ public DecompressingEntity(
         this.decoder = Args.notNull(decoder, "Stream decoder");
     }
 
+    private InputStream getDecompressingStream() throws IOException {
+        return new LazyDecompressingInputStream(super.getContent(), decoder);
+    }
+
     /**
      * Returns the cached <em>decoded</em> stream, creating it once if necessary.
      */
     @Override
     public InputStream getContent() throws IOException {
         if (!isStreaming()) {
-            return decoder.apply(super.getContent());
+            return getDecompressingStream();
         }
 
         InputStream local = cached;
         if (local == null) {
             if (cached == null) {
-                cached = decoder.apply(super.getContent());
+                cached = getDecompressingStream();
             }
             local = cached;
         }
@@ -97,4 +103,61 @@ public void writeTo(final OutputStream out) throws IOException {
             }
         }
     }
-}
+
+    private static final class LazyDecompressingInputStream extends FilterInputStream {
+
+        private final IOFunction<InputStream, InputStream> decoder;
+        private InputStream decompressedStream;
+
+        private LazyDecompressingInputStream(
+                final InputStream inputStream,
+                final IOFunction<InputStream, InputStream> decoder) {
+            super(inputStream);
+            this.decoder = decoder;
+        }
+
+        private InputStream getDecompressedStream() throws IOException {
+            if (decompressedStream == null) {
+                decompressedStream = decoder.apply(in);
+            }
+            return decompressedStream;
+        }
+
+        @Override
+        public int read() throws IOException {
+            return getDecompressedStream().read();
+        }
+
+        @Override
+        public int read(final byte[] b) throws IOException {
+            return getDecompressedStream().read(b);
+        }
+
+        @Override
+        public int read(final byte[] b, final int off, final int len) throws IOException {
+            return getDecompressedStream().read(b, off, len);
+        }
+
+        @Override
+        public long skip(final long n) throws IOException {
+            return getDecompressedStream().skip(n);
+        }
+
+        @Override
+        public int available() throws IOException {
+            return getDecompressedStream().available();
+        }
+
+        @Override
+        public void close() throws IOException {
+            final InputStream local = decompressedStream;
+            if (local != null) {
+                local.close();
+            } else {
+                super.close();
+            }
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java
index e19b61aca..a24e050e6 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java
@@ -29,11 +29,13 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.zip.CRC32;
 import java.util.zip.CheckedInputStream;
 import java.util.zip.Checksum;
+import java.util.zip.GZIPInputStream;
 
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpEntity;
@@ -108,6 +110,28 @@ void testWriteToStream() throws Exception {
         }
     }
 
+    @Test
+    void testMalformedGzipContentCanBeClosedWithoutReading() throws Exception {
+        final ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
+        final InputStreamEntity wrapped = new InputStreamEntity(in, -1, ContentType.APPLICATION_OCTET_STREAM);
+        final HttpEntity entity = new org.apache.hc.client5.http.entity.compress.DecompressingEntity(
+                wrapped, GZIPInputStream::new);
+
+        Assertions.assertDoesNotThrow(() -> entity.getContent().close());
+    }
+
+    @Test
+    void testMalformedGzipContentFailsOnRead() throws Exception {
+        final ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]);
+        final InputStreamEntity wrapped = new InputStreamEntity(in, -1, ContentType.APPLICATION_OCTET_STREAM);
+        final HttpEntity entity = new org.apache.hc.client5.http.entity.compress.DecompressingEntity(
+                wrapped, GZIPInputStream::new);
+
+        try (final InputStream content = entity.getContent()) {
+            Assertions.assertThrows(IOException.class, content::read);
+        }
+    }
+
     static class ChecksumEntity extends org.apache.hc.client5.http.entity.compress.DecompressingEntity {
 
         public ChecksumEntity(final HttpEntity wrapped, final Checksum checksum) {