diff --git a/vertx-db2-client/src/main/java/io/vertx/db2client/impl/codec/DB2Decoder.java b/vertx-db2-client/src/main/java/io/vertx/db2client/impl/codec/DB2Decoder.java
index 1df46b17b..65b30da8f 100644
--- a/vertx-db2-client/src/main/java/io/vertx/db2client/impl/codec/DB2Decoder.java
+++ b/vertx-db2-client/src/main/java/io/vertx/db2client/impl/codec/DB2Decoder.java
@@ -61,11 +61,17 @@ private int computeLength(ByteBuf in) {
dssContinues &= (in.getByte(ridx + index + 3) & 0x40) == 0x40;
else
dssContinues = false;
- short dssLen = 11; // minimum length of DRDA message
- if (readableBytes >= index + 2) // julien: is this correct ? thtis checks more space than necessary
- dssLen = in.getShort(ridx + index);
+ int dssLen = 11; // minimum length of DRDA message
+ if (readableBytes >= index + 2)
+ dssLen = in.getUnsignedShort(ridx + index);
index += dssLen;
}
+ if (dssContinues) {
+ // Chain bit was set on the last DSS we could fully read, but the next
+ // chained DSS has not arrived yet. Signal "need more data" so that
+ // decode() waits instead of processing a partial response.
+ return readableBytes + 1;
+ }
return index;
}
diff --git a/vertx-db2-client/src/test/java/io/vertx/db2client/impl/codec/DB2DecoderChainedDssTest.java b/vertx-db2-client/src/test/java/io/vertx/db2client/impl/codec/DB2DecoderChainedDssTest.java
new file mode 100644
index 000000000..b23c7e263
--- /dev/null
+++ b/vertx-db2-client/src/test/java/io/vertx/db2client/impl/codec/DB2DecoderChainedDssTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ * which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ */
+package io.vertx.db2client.impl.codec;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.Test;
+
+import java.util.ArrayDeque;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link DB2Decoder#computeLength(ByteBuf)} handling of chained
+ * DRDA DSS segments that arrive split across TCP deliveries.
+ *
+ * A DRDA response can consist of multiple DSS (Data Stream Structure) segments
+ * chained together via bit 6 (0x40) of byte 3 in each DSS header. When TCP
+ * delivers only the first segment(s) of a chained response, the decoder must
+ * wait for the remaining segments before attempting to process the response.
+ *
+ * DSS header format (6 bytes):
+ *
+ * bytes 0-1: length (unsigned 16-bit, big-endian)
+ * byte 2 : magic (0xD0)
+ * byte 3 : format flags (bit 6 = chain bit: 0x40 = chained, 0x00 = last)
+ * bytes 4-5: correlation ID
+ *
+ */
+public class DB2DecoderChainedDssTest {
+
+ private static final byte DSS_MAGIC = (byte) 0xD0;
+ private static final byte CHAIN = 0x42; // chain bit (0x40) + RPYDSS format (0x02)
+ private static final byte NO_CHAIN = 0x02; // no chain, RPYDSS format
+
+ private EmbeddedChannel createChannel() {
+ ArrayDeque> inflight = new ArrayDeque<>();
+ return new EmbeddedChannel(new DB2Decoder(inflight));
+ }
+
+ private ByteBuf buildDss(int length, boolean chained) {
+ ByteBuf buf = Unpooled.buffer(length);
+ buf.writeShort(length);
+ buf.writeByte(DSS_MAGIC);
+ buf.writeByte(chained ? CHAIN : NO_CHAIN);
+ buf.writeShort(1); // correlation ID
+ buf.writeZero(length - 6);
+ return buf;
+ }
+
+ /**
+ * When a chained DSS arrives but its successor has not been delivered yet,
+ * the decoder must wait for more data. Before the fix, computeLength()
+ * returned the length of just the first DSS (which equalled readableBytes),
+ * causing decode() to pass partial data to decodePayload(). With an empty
+ * inflight queue this manifested as a NullPointerException.
+ */
+ @Test
+ public void testChainedDssSplitAcrossTcpDeliveries() {
+ EmbeddedChannel channel = createChannel();
+
+ ByteBuf dss1 = buildDss(20, true);
+
+ // Feed only DSS_1 (chain bit set) without the following DSS_2.
+ // The decoder must NOT attempt to process this partial response.
+ assertFalse(
+ "Decoder must not produce output for incomplete chained response",
+ channel.writeInbound(dss1));
+
+ // No exception means the decoder correctly waited for more data.
+ // Before the fix, this would NPE in decodePayload() because
+ // inflight.peek() returned null.
+ assertNull("No inbound message expected", channel.readInbound());
+
+ channel.finishAndReleaseAll();
+ }
+
+ /**
+ * When only a partial DSS header is available (fewer than 4 bytes, so the
+ * chain bit cannot be read), the decoder must wait for more data.
+ */
+ @Test
+ public void testPartialDssHeader() {
+ EmbeddedChannel channel = createChannel();
+
+ // Only 3 bytes - not enough for a full 4-byte header check
+ ByteBuf partial = Unpooled.buffer(3);
+ partial.writeShort(20); // declared length = 20
+ partial.writeByte(DSS_MAGIC);
+
+ assertFalse(
+ "Decoder must wait when DSS header is incomplete",
+ channel.writeInbound(partial));
+
+ assertNull("No inbound message expected", channel.readInbound());
+
+ channel.finishAndReleaseAll();
+ }
+
+ /**
+ * When a chained DSS arrives with its successor partially delivered (only
+ * part of DSS_2's bytes are in the buffer), the decoder must still wait.
+ */
+ @Test
+ public void testChainedDssWithPartialSecondSegment() {
+ EmbeddedChannel channel = createChannel();
+
+ ByteBuf dss1 = buildDss(20, true);
+ // DSS_2 declares length 30 but only 10 bytes arrived so far
+ ByteBuf partialDss2 = Unpooled.buffer(10);
+ partialDss2.writeShort(30);
+ partialDss2.writeByte(DSS_MAGIC);
+ partialDss2.writeByte(NO_CHAIN);
+ partialDss2.writeShort(1);
+ partialDss2.writeZero(4);
+
+ ByteBuf combined = Unpooled.wrappedBuffer(dss1, partialDss2);
+
+ assertFalse(
+ "Decoder must wait when second DSS segment is incomplete",
+ channel.writeInbound(combined));
+
+ assertNull("No inbound message expected", channel.readInbound());
+
+ channel.finishAndReleaseAll();
+ }
+
+ /**
+ * Three-segment chain where only the first two segments have arrived.
+ * DSS_1 (chain) -> DSS_2 (chain) -> DSS_3 (not yet received).
+ */
+ @Test
+ public void testThreeSegmentChainWithMissingThird() {
+ EmbeddedChannel channel = createChannel();
+
+ ByteBuf dss1 = buildDss(16, true);
+ ByteBuf dss2 = buildDss(24, true); // chain bit still set
+
+ ByteBuf combined = Unpooled.wrappedBuffer(dss1, dss2);
+
+ assertFalse(
+ "Decoder must wait when third chained segment is missing",
+ channel.writeInbound(combined));
+
+ assertNull("No inbound message expected", channel.readInbound());
+
+ channel.finishAndReleaseAll();
+ }
+}