Skip to content

Commit e6bb4e8

Browse files
authored
Handle malformed exception response PDU in ModbusTcpClient (#123)
Strict interpretation of the spec would dictate that the response PDU is malformed if it's not exactly 2 bytes, but Modbus implementations in the wild are notoriously crap, so this approach that looks for at least 2 bytes might be a little safer and friendlier.
1 parent ec217a5 commit e6bb4e8

File tree

2 files changed

+37
-10
lines changed

2 files changed

+37
-10
lines changed

modbus/src/main/java/com/digitalpetri/modbus/client/ModbusTcpClient.java

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.digitalpetri.modbus.exceptions.ModbusExecutionException;
88
import com.digitalpetri.modbus.exceptions.ModbusResponseException;
99
import com.digitalpetri.modbus.exceptions.ModbusTimeoutException;
10+
import com.digitalpetri.modbus.internal.util.Hex;
1011
import com.digitalpetri.modbus.pdu.ModbusPdu;
1112
import com.digitalpetri.modbus.pdu.ModbusRequestPdu;
1213
import com.digitalpetri.modbus.pdu.ModbusResponsePdu;
@@ -181,17 +182,19 @@ private void onFrameReceived(ModbusTcpFrame frame) {
181182
int functionCode = buffer.get(buffer.position()) & 0xFF;
182183

183184
if (functionCode == promise.functionCode) {
184-
try {
185-
promise.future.complete(buffer);
186-
} catch (Exception e) {
187-
promise.future.completeExceptionally(e);
188-
}
185+
promise.future.complete(buffer);
189186
} else if (functionCode == promise.functionCode + 0x80) {
190-
buffer.get(); // skip FC byte
191-
int exceptionCode = buffer.get() & 0xFF;
192-
193-
promise.future.completeExceptionally(
194-
new ModbusResponseException(promise.functionCode, exceptionCode));
187+
if (buffer.remaining() >= 2) {
188+
buffer.get(); // skip FC byte
189+
int exceptionCode = buffer.get() & 0xFF;
190+
191+
promise.future.completeExceptionally(
192+
new ModbusResponseException(promise.functionCode, exceptionCode));
193+
} else {
194+
promise.future.completeExceptionally(
195+
new ModbusException(
196+
"malformed exception response PDU: %s".formatted(Hex.format(buffer))));
197+
}
195198
} else {
196199
promise.future.completeExceptionally(
197200
new ModbusException("unexpected function code: 0x%02X".formatted(functionCode)));

modbus/src/test/java/com/digitalpetri/modbus/client/ModbusTcpClientTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,30 @@ void emptyResponsePdu() {
3838
assertEquals("empty response PDU", cause.getMessage());
3939
}
4040

41+
/**
42+
* Tests handling of a malformed exception response PDU containing only the function code | 0x80
43+
* and missing the required exception code byte.
44+
*/
45+
@Test
46+
void malformedExceptionResponsePdu() {
47+
var transport = new TestTransport();
48+
var client = ModbusTcpClient.create(transport);
49+
50+
// Send a request with function code 0x04
51+
CompletionStage<byte[]> cs = client.sendRawAsync(1, new byte[] {0x04, 0x03, 0x00, 0x00, 0x01});
52+
53+
// Receive a malformed exception response: only 1 byte (0x84), no exception code
54+
transport.frameReceiver.accept(
55+
new ModbusTcpFrame(
56+
new MbapHeader(0, 1, 1, 1), ByteBuffer.wrap(new byte[] {(byte) 0x84})));
57+
58+
ExecutionException ex =
59+
assertThrows(ExecutionException.class, () -> cs.toCompletableFuture().get());
60+
61+
ModbusException cause = (ModbusException) ex.getCause();
62+
assertEquals("malformed exception response PDU: 84", cause.getMessage());
63+
}
64+
4165
private static class TestTransport implements ModbusTcpClientTransport {
4266

4367
boolean connected = false;

0 commit comments

Comments
 (0)