eclipse-paho / eclipse-paho/paho.mqtt.java
[BUG] Persisted alias-only QoS 1/2 PUBLISH causes RC 148 (reconnect) and RC 130 (restart) infinite disconnect loop — gap in #527 fix
- Dominant language
- Java
- Stars
- 2.3k
- Forks
- 919
- PR merge metrics
- No merged PRs in 30d
Description
## Version
1.2.5
## Related Issues
- #527 (closed — fix incomplete for this scenario)
- #866 (open — further evidence of ongoing topic alias defects in same version)
## Description
When `automaticReconnect=true`, `cleanStart=false`, and `MqttDefaultFilePersistence` is used, an automatic reconnect after a keepAlive timeout causes an immediate broker-initiated DISCONNECT with reason code **148 (Topic Alias Invalid)**, triggering an **infinite reconnect loop**. This affects **QoS 1 and QoS 2** messages alike, as both are persisted to disk when in-flight.
## Root Cause
Issue #527 was closed with a fix that reset the **in-memory** outgoing topic alias table on reconnect. However, that fix is incomplete for the following scenario:
When a QoS 1 or QoS 2 message carrying `topicAlias` in its `MqttProperties` is in-flight at the time of disconnection, Paho serializes that message to `MqttDefaultFilePersistence` — **including the `topicAlias` value and `topic=null`**. On reconnect, Paho re-delivers the message from disk with the stale `topicAlias=N, topic=null`. The new broker session has no alias mapping established, so it correctly rejects it per **MQTT v5 spec §3.3.2.3.4** and disconnects with RC 148, which immediately triggers another reconnect — repeating infinitely.
Per the MQTT v5 specification:
> *"A Topic Alias is an integer value that is used to identify the Topic instead of using the Topic Name. Topic Aliases are scoped to a Network Connection."*
Topic alias mappings are strictly **per-connection**, not per-session. They must be re-established from scratch on every new CONNECT regardless of `sessionPresent`. Paho's fix for #527 addresses the in-memory alias table but leaves persisted in-flight messages carrying stale aliases untouched.
## Evidence of Gap in #527 Fix
| Scenario | Covered by #527 fix | This bug |
|----------|-------------------|----------|
| New publish after reconnect uses stale alias from in-memory table | Fixed | N/A |
| Persisted in-flight message (QoS 1/2) re-delivered from disk with stale alias | Not fixed | This issue |
Additionally, the still-open issue **#866** ("fix topic alias Always empty", filed March 2021 against 1.2.5 and 1.2.6-SNAPSHOT) demonstrates that topic alias handling in `ClientState.java` continues to have unresolved defects beyond what #527 addressed.
## Steps to Reproduce
1. Connect with `cleanStart=false`, `automaticReconnect=true`, `MqttDefaultFilePersistence`, `keepAliveInterval=5`
2. Publish QoS 1 or QoS 2 messages with `topicAlias` set in `MqttProperties`
3. Simulate broker unavailability long enough to trigger a keepAlive timeout
4. Restore broker availability
5. Observe: Paho reconnects successfully (CONNACK returnCode=0) but immediately receives broker DISCONNECT RC 148, triggering another reconnect cycle
## Debug Log
### Step 1 — KeepAlive timeout: PINGREQ sent, no PINGRESP received
```
2026-06-18 13:54:18.679 DEBUG [MQTT Ping: Demo5] ClientState - ping needed. keepAlive=5,000,000,000 lastOutboundActivity=2,303,715,409,191,300 lastInboundActivity=2,303,715,399,737,400
2026-06-18 13:54:18.680 DEBUG [MQTT Snd: Demo5] CommsSender - network send key=Ping msg=PINGREQ
2026-06-18 13:54:18.681 DEBUG [MQTT Snd: Demo5] ClientState - ping sent. pingOutstanding: 1
-- No PINGRESP received. CommsReceiver polls for 5 seconds with no data --
2026-06-18 13:54:23.691 ERROR [MQTT Ping: Demo5] ClientState - Timed out as no activity,
keepAlive=5,000,000,000
lastOutboundActivity=2,303,725,398,704,300
lastInboundActivity=2,303,715,399,737,400 <-- 10s gap, two keepAlive periods
lastPing=2,303,720,413,051,800
org.eclipse.paho.mqttv5.common.MqttException: Timed out waiting for a response from the server
at org.eclipse.paho.mqttv5.client.internal.ClientState.checkForActivity(ClientState.java:736)
at org.eclipse.paho.mqttv5.client.TimerPingSender$PingTask.run(TimerPingSender.java:105)
```
### Step 2 — Message in-flight at time of disconnect: persisted with topicAlias=1, topic=null
```
2026-06-18 13:54:23.657 DEBUG [pool-16-thread-1] ClientState - pending publish key=6 qos=1
message=MqttPublish [qos=1, messageId=6, retained=false, duplicate=false,
topic=null,
payload=[utf8=Hello MQTT],
properties=[topicAlias=1, userProperties=[...], contentType=text/plain,
messageExpiryInterval=23423423, correlationData=[...]]]
2026-06-18 13:54:23.707 DEBUG [MQTT Ping: Demo5] ClientState - Clearing Connection State (Topic Aliases)
2026-06-18 13:54:23.712 DEBUG [MQTT Ping: Demo5] MqttAsyncClient - Start reconnect timer for client: Demo5, delay: 1,000
```
### Step 3 — Reconnect attempts with exponential backoff (broker down)
```
2026-06-18 13:54:24.713 DEBUG [MQTT Reconnect: Demo5] MqttAsyncClient - Triggering Automatic Reconnect attempt. delay=1000
2026-06-18 13:54:24.814 DEBUG [MQTT Rec: Demo5] MqttAsyncClient - Automatic Reconnect failed, rescheduling. delay=2000
2026-06-18 13:54:26.817 DEBUG [MQTT Reconnect: Demo5] MqttAsyncClient - Triggering Automatic Reconnect attempt. delay=2000
2026-06-18 13:54:27.886 DEBUG [MQTT Rec: Demo5] MqttAsyncClient - Automatic Reconnect failed, rescheduling. delay=4000
2026-06-18 13:54:31.889 DEBUG [MQTT Reconnect: Demo5] MqttAsyncClient - Triggering Automatic Reconnect attempt. delay=4000
2026-06-18 13:54:34.029 DEBUG [MQTT Rec: Demo5] MqttAsyncClient - Automatic Reconnect failed, rescheduling. delay=8000
2026-06-18 13:54:42.031 DEBUG [MQTT Reconnect: Demo5] MqttAsyncClient - Triggering Automatic Reconnect attempt. delay=8000
```
### Step 4 — Reconnect succeeds, CONNACK sessionPresent=false, stale alias re-delivered immediately
```
2026-06-18 13:54:42.368 DEBUG [MQTT Rec: Demo5] MqttInputStream - Received MqttConnAck
[returnCode=0, sessionPresent=false,
properties=[receiveMaximum=10, maximumPacketSize=10048576, topicAliasMaximum=5]]
-- Paho re-delivers persisted in-flight message from disk BEFORE connectComplete fires --
2026-06-18 13:54:42.370 DEBUG [MQTT Snd: Demo5] CommsSender - network send key=6
msg=MqttPublish [qos=1, messageId=6, retained=false, duplicate=true,
topic=null, <-- topic is null, alias-only
properties=[topicAlias=1, <-- stale alias from previous connection
userProperties=[...], contentType=text/plain,
messageExpiryInterval=23423423]]
2026-06-18 13:54:42.370 DEBUG [MQTT Call: Demo5] MqttAsyncClient - Automatic Reconnect Successful: Demo5
```
### Step 5 — Broker disconnects with RC 148, infinite loop begins
```
2026-06-18 13:54:42.373 DEBUG [MQTT Rec: Demo5] MqttInputStream - Received MqttDisconnect
[returnCode=148,
properties=[reasonString=Topic alias in PUBLISH could not be mapped.]]
2026-06-18 13:54:42.376 DEBUG [MQTT Rec: Demo5] ClientState - reason:
The Server Disconnected the client. Disconnect RC: 148
Disconnect Reason: Topic alias in PUBLISH could not be mapped. (32204)
org.eclipse.paho.mqttv5.common.MqttException: The Server Disconnected the client.
Disconnect RC: 148 Disconnect Reason: Topic alias in PUBLISH could not be mapped.
at org.eclipse.paho.mqttv5.client.internal.CommsReceiver.run(CommsReceiver.java:162)
2026-06-18 13:54:42.377 DEBUG [MQTT Rec: Demo5] ClientState - Clearing Connection State (Topic Aliases)
2026-06-18 13:54:42.380 DEBUG [MQTT Rec: Demo5] MqttAsyncClient - Start reconnect timer for client: Demo5, delay: 1,000
-- Cycle repeats: reconnect -> CONNACK -> re-deliver stale alias message -> RC 148 -> reconnect --
```
**Key observation:** The log line `Clearing Connection State (Topic Aliases)` appears on every disconnect — confirming the in-memory table IS cleared by the #527 fix. But the persisted message on disk still carries `topicAlias=1, topic=null` and is re-sent unchanged on the next reconnect, proving the fix is incomplete for the persistence path.
## Expected Behavior
Before re-delivering any persisted in-flight message from `MqttDefaultFilePersistence` on reconnect, Paho should:
1. Detect the presence of a `topicAlias` in the message properties
2. Strip the alias
3. Restore the full topic name from the token store (topic is tracked separately in `CommsTokenStore`)
This must happen regardless of `sessionPresent` since topic alias mappings are connection-scoped per the MQTT v5 specification and are **never** valid after a new CONNECT.
## Workaround
- Use `cleanStart=true` — prevents persisted in-flight retry, but sacrifices session continuity
- Avoid setting `topicAlias` on messages — eliminates the trigger but forgoes the bandwidth optimization
- Neither is acceptable in production scenarios requiring both reliable delivery and efficient topic alias use
Application Debug Logs:
[AppDebugLogs.log](https://github.com/user-attachments/files/29089543/AppDebugLogs.log)
Contributor guide
Research direction
Start by tracing the reconnect and persisted in-flight publish handling in ClientState.java, then inspect how MqttDefaultFilePersistence and CommsTokenStore retain topic information. Reproduce the keepAlive timeout with QoS 1 or QoS 2 and verify that reconnecting no longer re-sends an alias-only publish with a stale topic alias or enters the RC 148 loop.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100