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

Open
#1,104 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.