Repository: pymodbus-dev/pymodbus

Issue:
AsyncModbusTcpClient never recovers after bad response
I have a modbus server that occasionally returns an invalid response to read-holding-registers, where the "number of bytes" number in the PDU is higher than the actual number of bytes in the response. AsyncModbusTcpClient reacts by disconnecting — which is fine — but after automatically reconnecting it is unable to handle further responses.

From what I can see: ReadRegistersResponseBase.decode() crashes in struct.unpack due to lack of data, because the length indication in the packet was incorrect. This exception isn't caught anywhere and the connection is closed. Upon reconnection, the ModbusSocketFramer is not reset, so when the next response comes in we try to parse the old broken response again. This repeats forever, the framer buffer getting larger every time.

Python: 3.10.9 (also tested 3.9.16)
OS: Linux
Pymodbus: 3.2.2 (also tested dev2.x-543-gd347bc8)


### Code and Logs

Reproducer client
```
#!/usr/bin/env python
import asyncio
import logging

import pymodbus
from pymodbus.client import AsyncModbusTcpClient

pymodbus.pymodbus_apply_logging_config()
log = logging.getLogger()

async def main():
    client = AsyncModbusTcpClient('localhost', port=5020)
    await client.connect()

    for i in range(7):
        log.info(f'{i}: Sending request on client {client}')
        try:
            result = await client.read_holding_registers(3000, 1)
            log.info(f'{i}: {result}')
        except Exception as e:
            log.error(f'{i}: {e}')
        while not client.connected:
            await asyncio.sleep(0.1)

asyncio.run(main())
```

Reproducer server
```
#!/usr/bin/env python
import socketserver
import struct
import logging

log = logging.getLogger()


class Handler(socketserver.StreamRequestHandler):
    def handle(self):
        log.info('New connection')
        while True:
            data = self.request.recv(1024)
            if not data:
                break
            tid, pid, length, unit, fc = struct.unpack_from('>HHHBB', data)
            pdu = data[8:8+length]
            log.debug(f'Got {(tid, pid, length, unit, fc)}, PDU {pdu}')
            if fc == 3 or fc == 4:
                address, count = struct.unpack('>HH', pdu)
                header_len = count * 2 + 3
                registers_len = count * 2
                if tid == 4:
                    log.info('Sending wrong length')
                    registers_len += 8
                response = struct.pack('>HHHBB', tid, pid, header_len, unit, fc)
                response += struct.pack('>B', registers_len)
                response += struct.pack('>H', tid) * count
                self.request.send(response)
            else:
                break
        self.request.close()

class Server(socketserver.ThreadingMixIn, socketserver.TCPServer):
    daemon_threads = True
    allow_reuse_address = True


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG)
    server = Server(('0.0.0.0', 5020), Handler)
    server.serve_forever()
```

Log
```
$ tests/modbus_reproducer.py 
2023-05-24 09:17:48,458 DEBUG logging:102 Connecting to localhost:5020.
2023-05-24 09:17:48,458 DEBUG logging:102 Connecting.
2023-05-24 09:17:48,463 DEBUG logging:102 Connected comm
2023-05-24 09:17:48,463 INFO  logging:96 Connected to localhost:5020.
2023-05-24 09:17:48,464 DEBUG logging:102 send: 0x0 0x1 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:48,464 DEBUG logging:102 Adding transaction 1
2023-05-24 09:17:48,464 DEBUG logging:102 recv: 0x0 0x1 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x1
2023-05-24 09:17:48,464 DEBUG logging:102 Processing: 0x0 0x1 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x1
2023-05-24 09:17:48,464 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:48,464 DEBUG logging:102 Getting transaction 1
2023-05-24 09:17:48,464 DEBUG logging:102 send: 0x0 0x2 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:48,464 DEBUG logging:102 Adding transaction 2
2023-05-24 09:17:48,465 DEBUG logging:102 recv: 0x0 0x2 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x2
2023-05-24 09:17:48,465 DEBUG logging:102 Processing: 0x0 0x2 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x2
2023-05-24 09:17:48,465 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:48,465 DEBUG logging:102 Getting transaction 2
2023-05-24 09:17:48,465 DEBUG logging:102 send: 0x0 0x3 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:48,465 DEBUG logging:102 Adding transaction 3
2023-05-24 09:17:48,465 DEBUG logging:102 recv: 0x0 0x3 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x3
2023-05-24 09:17:48,465 DEBUG logging:102 Processing: 0x0 0x3 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x3
2023-05-24 09:17:48,465 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:48,465 DEBUG logging:102 Getting transaction 3
2023-05-24 09:17:48,466 DEBUG logging:102 send: 0x0 0x4 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:48,466 DEBUG logging:102 Adding transaction 4
2023-05-24 09:17:48,466 DEBUG logging:102 recv: 0x0 0x4 0x0 0x0 0x0 0x5 0x0 0x3 0xa 0x0 0x4
2023-05-24 09:17:48,466 DEBUG logging:102 Processing: 0x0 0x4 0x0 0x0 0x0 0x5 0x0 0x3 0xa 0x0 0x4
2023-05-24 09:17:48,466 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:48,466 ERROR logging:114 General exception: unpack requires a buffer of 2 bytes
Fatal error: protocol.data_received() call failed.
protocol: <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7fcec3da44c0>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib64/python3.9/asyncio/selector_events.py", line 868, in _read_ready__data_received
    self._protocol.data_received(data)
  File "/home/jonasn/src/pymodbus/pymodbus/client/base.py", line 207, in data_received
    self.framer.processIncomingPacket(data, self._handle_response, slave=0)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 173, in processIncomingPacket
    self._process(callback, tid)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 179, in _process
    raise ModbusIOException("Unable to decode request")
pymodbus.exceptions.ModbusIOException: Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:48,467 DEBUG logging:102 Connection lost comm due to Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:48,467 DEBUG logging:102 Getting transaction 4
2023-05-24 09:17:48,467 DEBUG logging:102 Waiting 100 ms reconnecting.
3: Modbus Error: [Connection] Connection lost during request
2023-05-24 09:17:48,568 DEBUG logging:102 Connecting to localhost:5020.
2023-05-24 09:17:48,568 DEBUG logging:102 Connecting.
2023-05-24 09:17:48,569 DEBUG logging:102 Connected comm
2023-05-24 09:17:48,569 INFO  logging:96 Connected to localhost:5020.
2023-05-24 09:17:48,669 DEBUG logging:102 send: 0x0 0x5 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:48,670 DEBUG logging:102 Adding transaction 5
2023-05-24 09:17:48,670 DEBUG logging:102 recv: 0x0 0x5 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x5
2023-05-24 09:17:48,670 DEBUG logging:102 Processing: 0x0 0x5 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x5
2023-05-24 09:17:48,670 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:48,670 ERROR logging:114 General exception: unpack requires a buffer of 2 bytes
Fatal error: protocol.data_received() call failed.
protocol: <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7fcec3da44c0>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib64/python3.9/asyncio/selector_events.py", line 868, in _read_ready__data_received
    self._protocol.data_received(data)
  File "/home/jonasn/src/pymodbus/pymodbus/client/base.py", line 207, in data_received
    self.framer.processIncomingPacket(data, self._handle_response, slave=0)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 173, in processIncomingPacket
    self._process(callback, tid)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 179, in _process
    raise ModbusIOException("Unable to decode request")
pymodbus.exceptions.ModbusIOException: Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:48,670 DEBUG logging:102 Connection lost comm due to Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:48,670 DEBUG logging:102 Getting transaction 5
2023-05-24 09:17:48,670 DEBUG logging:102 Waiting 100 ms reconnecting.
4: Modbus Error: [Connection] Connection lost during request
2023-05-24 09:17:48,771 DEBUG logging:102 Connecting to localhost:5020.
2023-05-24 09:17:48,771 DEBUG logging:102 Connecting.
2023-05-24 09:17:48,772 DEBUG logging:102 Connected comm
2023-05-24 09:17:48,772 INFO  logging:96 Connected to localhost:5020.
2023-05-24 09:17:48,871 DEBUG logging:102 send: 0x0 0x6 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:48,872 DEBUG logging:102 Adding transaction 6
2023-05-24 09:17:48,872 DEBUG logging:102 recv: 0x0 0x6 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x6
2023-05-24 09:17:48,872 DEBUG logging:102 Processing: 0x0 0x6 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x6
2023-05-24 09:17:48,872 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:48,872 ERROR logging:114 General exception: unpack requires a buffer of 2 bytes
Fatal error: protocol.data_received() call failed.
protocol: <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7fcec3da44c0>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib64/python3.9/asyncio/selector_events.py", line 868, in _read_ready__data_received
    self._protocol.data_received(data)
  File "/home/jonasn/src/pymodbus/pymodbus/client/base.py", line 207, in data_received
    self.framer.processIncomingPacket(data, self._handle_response, slave=0)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 173, in processIncomingPacket
    self._process(callback, tid)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 179, in _process
    raise ModbusIOException("Unable to decode request")
pymodbus.exceptions.ModbusIOException: Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:48,872 DEBUG logging:102 Connection lost comm due to Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:48,872 DEBUG logging:102 Getting transaction 6
2023-05-24 09:17:48,872 DEBUG logging:102 Waiting 100 ms reconnecting.
5: Modbus Error: [Connection] Connection lost during request
2023-05-24 09:17:48,973 DEBUG logging:102 Connecting to localhost:5020.
2023-05-24 09:17:48,973 DEBUG logging:102 Connecting.
2023-05-24 09:17:48,976 DEBUG logging:102 Connected comm
2023-05-24 09:17:48,976 INFO  logging:96 Connected to localhost:5020.
2023-05-24 09:17:49,075 DEBUG logging:102 send: 0x0 0x7 0x0 0x0 0x0 0x6 0x0 0x3 0xb 0xb8 0x0 0x1
2023-05-24 09:17:49,075 DEBUG logging:102 Adding transaction 7
2023-05-24 09:17:49,076 DEBUG logging:102 recv: 0x0 0x7 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x7
2023-05-24 09:17:49,076 DEBUG logging:102 Processing: 0x0 0x7 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x7
2023-05-24 09:17:49,076 DEBUG logging:102 Factory Response[ReadHoldingRegistersResponse': 3]
2023-05-24 09:17:49,076 ERROR logging:114 General exception: unpack requires a buffer of 2 bytes
Fatal error: protocol.data_received() call failed.
protocol: <pymodbus.client.tcp.AsyncModbusTcpClient object at 0x7fcec3da44c0>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib64/python3.9/asyncio/selector_events.py", line 868, in _read_ready__data_received
    self._protocol.data_received(data)
  File "/home/jonasn/src/pymodbus/pymodbus/client/base.py", line 207, in data_received
    self.framer.processIncomingPacket(data, self._handle_response, slave=0)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 173, in processIncomingPacket
    self._process(callback, tid)
  File "/home/jonasn/src/pymodbus/pymodbus/framer/socket_framer.py", line 179, in _process
    raise ModbusIOException("Unable to decode request")
pymodbus.exceptions.ModbusIOException: Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:49,077 DEBUG logging:102 Connection lost comm due to Modbus Error: [Input/Output] Unable to decode request
2023-05-24 09:17:49,077 DEBUG logging:102 Getting transaction 7
2023-05-24 09:17:49,077 DEBUG logging:102 Waiting 100 ms reconnecting.
6: Modbus Error: [Connection] Connection lost during request
2023-05-24 09:17:49,178 DEBUG logging:102 Connecting to localhost:5020.
2023-05-24 09:17:49,178 DEBUG logging:102 Connecting.
2023-05-24 09:17:49,179 DEBUG logging:102 Connected comm
2023-05-24 09:17:49,179 INFO  logging:96 Connected to localhost:5020.
```


Discussion:
If it only sends extra bytes from time to time, the automatic reconnect clears the buffer (in both client and server) so that works until next time the client receives trash bytes.

Try to test with dev, because we added code recently (not released) to solve a similar problem (server on multi drop line), and I expect it to solve your problem as well. It will be released early next month as version 3.3


I will try to make a test case with your example and see if I can statically reproduce it and if so solve it.
The buffer does not get bigger and bigger:
This causes the exception (which is a real bug):
´´´
2023-05-24 09:17:48,466 DEBUG logging:102 recv: 0x0 0x4 0x0 0x0 0x0 0x5 0x0 0x3 0xa 0x0 0x4
2023-05-24 09:17:48,466 DEBUG logging:102 Processing: 0x0 0x4 0x0 0x0 0x0 0x5 0x0 0x3 0xa 0x0 0x4
´´´
After reconnect, a request is sent and the response is:
´´´
2023-05-24 09:17:48,872 DEBUG logging:102 recv: 0x0 0x6 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x6
2023-05-24 09:17:48,872 DEBUG logging:102 Processing: 0x0 0x6 0x0 0x0 0x0 0x5 0x0 0x3 0x2 0x0 0x6
´´´
As you can see "Processing" have nothing in common. The problem is that your device sends a faulty response again after the reconnect. 

Let me see what I can do to cure the faulty response.

Resolving patch:
diff --git a/pymodbus/framer/socket_framer.py b/pymodbus/framer/socket_framer.py
--- a/pymodbus/framer/socket_framer.py
+++ b/pymodbus/framer/socket_framer.py
@@ -176,6 +176,7 @@ def _process(self, callback, tid, error=False):
         """Process incoming packets irrespective error condition."""
         data = self.getRawFrame() if error else self.getFrame()
         if (result := self.decoder.decode(data)) is None:
+            self.resetFrame()
             raise ModbusIOException("Unable to decode request")
         if error and result.function_code < 0x80:
             raise InvalidMessageReceivedException(result)


Test patch:
diff --git a/test/test_client_faulty_response.py b/test/test_client_faulty_response.py
new file mode 100644
--- /dev/null
+++ b/test/test_client_faulty_response.py
@@ -0,0 +1,40 @@
+"""Test server working as slave on a multidrop RS485 line."""
+from unittest import mock
+
+import pytest
+
+from pymodbus.exceptions import ModbusIOException
+from pymodbus.factory import ClientDecoder
+from pymodbus.framer import ModbusSocketFramer
+
+
+class TestFaultyResponses:
+    """Test that server works on a multidrop line."""
+
+    slaves = [0]
+
+    good_frame = b"\x00\x01\x00\x00\x00\x05\x00\x03\x02\x00\x01"
+
+    @pytest.fixture(name="framer")
+    def fixture_framer(self):
+        """Prepare framer."""
+        return ModbusSocketFramer(ClientDecoder())
+
+    @pytest.fixture(name="callback")
+    def fixture_callback(self):
+        """Prepare dummy callback."""
+        return mock.Mock()
+
+    def test_ok_frame(self, framer, callback):
+        """Test ok frame."""
+        framer.processIncomingPacket(self.good_frame, callback, self.slaves)
+        callback.assert_called_once()
+
+    def test_faulty_frame1(self, framer, callback):
+        """Test ok frame."""
+        faulty_frame = b"\x00\x04\x00\x00\x00\x05\x00\x03\x0a\x00\x04"
+        with pytest.raises(ModbusIOException):
+            framer.processIncomingPacket(faulty_frame, callback, self.slaves)
+        callback.assert_not_called()
+        framer.processIncomingPacket(self.good_frame, callback, self.slaves)
+        callback.assert_called_once()
diff --git a/test/test_client_multidrop.py b/test/test_server_multidrop.py
similarity index 100%
rename from test/test_client_multidrop.py
rename to test/test_server_multidrop.py