From 744679ce50587ab44d10742f933e0906aacb5e31 Mon Sep 17 00:00:00 2001 From: Mohammed Hassan Date: Sun, 15 Nov 2026 15:22:08 +0530 Subject: [PATCH] handle BFINAL deflate blocks in permessage-deflate decoders PerMessageDeflateExtension.decode looped while the Inflater did not need input. After a DEFLATE block with BFINAL set, which RFC 7692 section 7.2.3.4 allows, the Inflater is finished and returns 0 while the trailing bytes stay unconsumed, so the loop never exits and a single compressed frame pins the server thread. The client-side PerMessageDeflate decoder did stop, but never reset the finished Inflater, so later messages decoded to empty payloads. Stop inflating once the stream is finished and reset the Inflater so the next DEFLATE stream decodes. --- .../websocket/PerMessageDeflateExtension.java | 7 ++++++- .../websocket/extension/PerMessageDeflate.java | 4 +++- .../websocket/PerMessageDeflateExtensionTest.java | 15 +++++++++++++++ .../websocket/extension/MessageDeflateTest.java | 13 +++++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/PerMessageDeflateExtension.java b/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/PerMessageDeflateExtension.java index 957a7f600c..aa5ef0d294 100644 --- a/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/PerMessageDeflateExtension.java +++ b/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/PerMessageDeflateExtension.java @@ -106,7 +106,7 @@ public ByteBuffer decode(final WebSocketFrameType type, final byte[] buffer = new byte[Math.min(16384, Math.max(1024, input.length * 2))]; long produced = 0L; try { - while (!inflater.needsInput()) { + while (!inflater.needsInput() && !inflater.finished()) { final int count = inflater.inflate(buffer); if (count == 0 && inflater.needsInput()) { break; @@ -124,6 +124,11 @@ public ByteBuffer decode(final WebSocketFrameType type, } catch (final Exception ex) { throw new WebSocketException("Unable to inflate payload", ex); } + if (inflater.finished()) { + // A DEFLATE block with BFINAL set ends the stream (RFC 7692 section 7.2.3.4); + // the sender starts a new one for the data that follows. + inflater.reset(); + } if (fin) { decodingMessage = false; if (clientNoContextTakeover) { diff --git a/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/extension/PerMessageDeflate.java b/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/extension/PerMessageDeflate.java index f18f2e39c7..3d994167ec 100644 --- a/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/extension/PerMessageDeflate.java +++ b/httpclient5-websocket/src/main/java/org/apache/hc/core5/websocket/extension/PerMessageDeflate.java @@ -190,7 +190,9 @@ public byte[] decode(final byte[] compressedMessage, final long maxDecodedSize) break; } } - if (serverNoContextTakeover) { + // A DEFLATE block with BFINAL set ends the stream (RFC 7692 section 7.2.3.4); + // the next message then starts a new one. + if (serverNoContextTakeover || inf.finished()) { inf.reset(); } return out.toByteArray(); diff --git a/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/PerMessageDeflateExtensionTest.java b/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/PerMessageDeflateExtensionTest.java index b561fbd700..de96e344ef 100644 --- a/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/PerMessageDeflateExtensionTest.java +++ b/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/PerMessageDeflateExtensionTest.java @@ -29,10 +29,12 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Arrays; import java.util.zip.Deflater; @@ -96,6 +98,19 @@ void decodeZeroLimitMeansUnlimited() throws Exception { assertArrayEquals(plain, toBytes(out)); } + @Test + void decodesMessagesEndingWithBfinalBlock() { + // RFC 7692 section 7.2.3.4: "Hello" compressed using a DEFLATE block with BFINAL set to 1. + final byte[] compressed = {(byte) 0xf3, 0x48, (byte) 0xcd, (byte) 0xc9, (byte) 0xc9, 0x07, 0x00, 0x00}; + final byte[] plain = "Hello".getBytes(StandardCharsets.UTF_8); + + final PerMessageDeflateExtension ext = new PerMessageDeflateExtension(); + assertTimeoutPreemptively(Duration.ofSeconds(5), () -> { + assertArrayEquals(plain, toBytes(ext.decode(WebSocketFrameType.TEXT, true, ByteBuffer.wrap(compressed)))); + assertArrayEquals(plain, toBytes(ext.decode(WebSocketFrameType.TEXT, true, ByteBuffer.wrap(compressed)))); + }); + } + @Test void fragmentedEncodeRoundTripsThroughDecode() throws Exception { // Encode a message as three frames (fin=false, false, true), then decode them back. diff --git a/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/extension/MessageDeflateTest.java b/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/extension/MessageDeflateTest.java index b1235cf05d..45f5b0f9ad 100644 --- a/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/extension/MessageDeflateTest.java +++ b/httpclient5-websocket/src/test/java/org/apache/hc/core5/websocket/extension/MessageDeflateTest.java @@ -125,6 +125,19 @@ void encode_emptyMessage_withNoContextTakeover_roundTrips() throws Exception { assertArrayEquals(new byte[0], dec.decode(empty)); } + @Test + void decode_messagesEndingWithBfinalBlock_withContextTakeover() throws Exception { + // RFC 7692 section 7.2.3.4: "Hello" compressed using a DEFLATE block with BFINAL set to 1. + final byte[] wire = {(byte) 0xf3, 0x48, (byte) 0xcd, (byte) 0xc9, (byte) 0xc9, 0x07, 0x00, 0x00}; + final byte[] plain = "Hello".getBytes(StandardCharsets.UTF_8); + + final PerMessageDeflate pmce = new PerMessageDeflate(true, false, false, null, null); + final WebSocketExtensionChain.Decoder dec = pmce.newDecoder(); + + assertArrayEquals(plain, dec.decode(wire)); + assertArrayEquals(plain, dec.decode(wire), "message after a BFINAL block must not decode to empty"); + } + @Test void decode_withinLimit_succeeds() throws Exception { final PerMessageDeflate pmce = new PerMessageDeflate(true, true, true, null, null);