From b4de5c8deded4c41559bd5dab315552cad0ab021 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Wed, 27 May 2026 07:07:23 +0200 Subject: [PATCH] HTTPCLIENT-2422: Restore lazy content decompression Defer construction of the decompression stream until the response content is actually read. --- .../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 070663ccc2..38b00a3aca 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 decoded 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 decoder; + private InputStream decompressedStream; + + private LazyDecompressingInputStream( + final InputStream inputStream, + final IOFunction 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 e19b61aca7..a24e050e6f 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) {