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

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-http.git


The following commit(s) were added to refs/heads/main by this push:
     new a06ad5738 GzipDecompressor: fix inflater memory leak (#1134)
a06ad5738 is described below

commit a06ad57386088fbf1bf9a18ba10bec67eae55385
Author: PJ Fanning <[email protected]>
AuthorDate: Wed Jul 8 09:28:45 2026 +0100

    GzipDecompressor: fix inflater memory leak (#1134)
    
    * Initial plan
    
    * Apply remaining changes
    
    * Update GzipSpec.scala
    
    * Update GzipCompressor.scala
    
    ---------
    
    Co-authored-by: copilot-swe-agent[bot] 
<[email protected]>
---
 .../pekko/http/scaladsl/coding/GzipSpec.scala      | 59 +++++++++++++++++++++-
 .../http/scaladsl/coding/GzipCompressor.scala      | 18 ++++++-
 2 files changed, 74 insertions(+), 3 deletions(-)

diff --git 
a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/coding/GzipSpec.scala
 
b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/coding/GzipSpec.scala
index ada29ac59..c7f749201 100644
--- 
a/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/coding/GzipSpec.scala
+++ 
b/http-tests/src/test/scala/org/apache/pekko/http/scaladsl/coding/GzipSpec.scala
@@ -14,10 +14,18 @@
 package org.apache.pekko.http.scaladsl.coding
 
 import java.io.{ InputStream, OutputStream }
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.atomic.AtomicInteger
 import java.util.zip.{ GZIPInputStream, GZIPOutputStream, ZipException }
 
+import scala.concurrent.duration._
+
 import org.apache.pekko
 import pekko.http.impl.util._
+import pekko.http.scaladsl.model.headers.HttpEncodings
+import pekko.stream.SystemMaterializer
+import pekko.stream.scaladsl.{ Sink, Source }
+import pekko.testkit.TestDuration
 import pekko.util.ByteString
 
 class GzipSpec extends CoderSpec {
@@ -43,13 +51,62 @@ class GzipSpec extends CoderSpec {
       ex.ultimateCause.getMessage should equal("Truncated GZIP stream")
     }
     "throw an error if compressed data is just missing the trailer at the end" 
in {
-      def brokenCompress(payload: String) = 
Coders.Gzip.newCompressor.compress(ByteString(payload, "UTF-8"))
+      def brokenCompress(payload: String) =
+        Coders.Gzip.newCompressor.compress(ByteString(payload, 
StandardCharsets.UTF_8))
       val ex = the[RuntimeException] thrownBy 
ourDecode(brokenCompress("abcdefghijkl"))
       ex.ultimateCause.getMessage should equal("Truncated GZIP stream")
     }
+    "release the inflater when decoding completes" in {
+      val inflater = new TrackingInflater
+
+      decodeWith(inflater, streamEncode(smallTextBytes)) should 
readAs(smallText)
+      inflater.endCalls.get() shouldEqual 1
+    }
+    "release the inflater when decoding is cancelled early" in {
+      val inflater = new TrackingInflater
+      val compressed = streamEncode(largeTextBytes)
+
+      Source.single(compressed)
+        .via(decoderWith(inflater).withMaxBytesPerChunk(1).decoderFlow)
+        .take(1)
+        .runWith(Sink.ignore)
+        .awaitResult(3.seconds.dilated)
+
+      inflater.endCalls.get() shouldEqual 1
+    }
+    "release the inflater when decoding fails on truncation" in {
+      val inflater = new TrackingInflater
+
+      val ex = the[RuntimeException] thrownBy decodeWith(inflater, 
streamEncode(smallTextBytes).dropRight(5))
+      ex.ultimateCause.getMessage should equal("Truncated GZIP stream")
+      inflater.endCalls.get() shouldEqual 1
+    }
     "throw early if header is corrupt" in {
       val cause = (the[RuntimeException] thrownBy ourDecode(ByteString(0, 1, 
2, 3, 4))).ultimateCause
       cause should ((be(a[ZipException]) and have).message("Not in GZIP 
format"))
     }
   }
+
+  private def decodeWith(inflater: TrackingInflater, bytes: ByteString): 
ByteString =
+    
decoderWith(inflater).decode(bytes)(SystemMaterializer(system).materializer).awaitResult(3.seconds.dilated)
+
+  private def decoderWith(inflater: TrackingInflater): StreamDecoder =
+    new StreamDecoder {
+      override val encoding = HttpEncodings.gzip
+
+      override def newDecompressorStage(maxBytesPerChunk: Int) =
+        () =>
+          new GzipDecompressor(maxBytesPerChunk) {
+            override protected[coding] def createInflater() = inflater
+          }
+    }
+
+  private class TrackingInflater extends java.util.zip.Inflater(true) {
+    val endCalls = new AtomicInteger
+
+    override def end(): Unit = {
+      endCalls.incrementAndGet()
+      super.end()
+    }
+  }
 }
diff --git 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/coding/GzipCompressor.scala
 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/coding/GzipCompressor.scala
index ab8b07ab5..78568d09a 100644
--- 
a/http/src/main/scala/org/apache/pekko/http/scaladsl/coding/GzipCompressor.scala
+++ 
b/http/src/main/scala/org/apache/pekko/http/scaladsl/coding/GzipCompressor.scala
@@ -70,12 +70,26 @@ private[coding] object GzipCompressor {
 @InternalApi
 private[coding] class GzipDecompressor(
     maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends 
DeflateDecompressorBase(maxBytesPerChunk) {
+  protected[coding] def createInflater(): Inflater = new Inflater(true)
+
   override def createLogic(attr: Attributes) = new ParsingLogic {
-    private val inflater = new Inflater(true)
+    private val inflater = createInflater()
     private val crc32: CRC32 = new CRC32
+    private var inflaterEnded = false
+
+    private def cleanupInflater(): Unit =
+      if (!inflaterEnded) {
+        inflaterEnded = true
+        inflater.end()
+      }
+
+    override def postStop(): Unit = cleanupInflater()
 
     trait Step extends ParseStep[ByteString] {
-      override def onTruncation(): Unit = failStage(new 
ZipException("Truncated GZIP stream"))
+      override def onTruncation(): Unit = {
+        cleanupInflater()
+        failStage(new ZipException("Truncated GZIP stream"))
+      }
     }
     startWith(ReadHeaders)
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to