Server: DISCONNECT with DisconnectWithWillMessage reason code is ignored — Will message is not published
- Dominant language
- C#
- Stars
- 5.1k
- Forks
- 1.2k
- PR merge metrics
- No merged PRs in 30d
Description
### Verification
1. Not applicable — this is a protocol-correctness bug (a Will message that should be delivered is silently dropped), not a performance/memory/CPU issue.
2. Not applicable, same reason as above.
3. Reproduced with two independent, unmodified `MQTTnet` clients talking to an in-process `MQTTnet.Server` instance — no application-specific wrapper code involved (repro below uses only `MQTTnet` / `MQTTnet.Server` APIs directly).
4. Confirmed present on the latest `master` (`Source/MQTTnet.Server/Internal/MqttConnectedClient.cs`, lines 115-117 as of this writing) as well as on the released NuGet package version 5.1.0.1559.
### Describe the bug
When a client sends `DISCONNECT` with reason code `0x04` (`DisconnectWithWillMessage`) — telling the server to publish its Will Message even though this is a clean, client-initiated disconnect (MQTT 5.0 §3.14.2.1) — the server never publishes the Will. It behaves identically to a plain `NormalDisconnection`.
The cause is in `MqttConnectedClient.RunAsync`:
```csharp
var isCleanDisconnect = DisconnectPacket != null;
if (!IsTakenOver && !isCleanDisconnect && Session.LatestConnectPacket.WillFlag && !Session.WillMessageSent)
```
`isCleanDisconnect` only checks whether *any* DISCONNECT packet was received — it never inspects `DisconnectPacket.ReasonCode`. So any client-sent DISCONNECT suppresses the Will, regardless of which reason code it carries. Per the spec, only reason codes *other than* `0x04` should suppress it; `DisconnectWithWillMessage` should still trigger delivery.
### Which component is your bug related to?
- Server
### To Reproduce
Steps to reproduce the behavior:
1. Using this version of MQTTnet `5.1.0.1559` (also reproduces on latest `master`).
2. Run this code:
- Start an `MqttServer` on a local port.
- Connect client `receiver`, subscribe to `#`.
- Connect client `sender` with a Will registered (`WithWillTopic`/`WithWillPayload`).
- Call `sender.DisconnectAsync(MqttClientDisconnectOptionsReason.DisconnectWithWillMessage)`.
3. With these arguments: any Will topic/payload; QoS 0 or 1, retain or not — reproduces regardless.
4. See error: `receiver` never gets the Will message. (For contrast, calling `sender.Dispose()` instead — an ungraceful drop with no DISCONNECT packet at all — *does* deliver the Will, confirming the Will registration itself is fine; only the explicit "disconnect with will" request is ignored.)
### Expected behavior
`receiver` should receive the Will message on `sender`'s configured Will topic, because `sender` explicitly disconnected with reason code `DisconnectWithWillMessage`.
### Screenshots
Not applicable.
### Additional context / logging
```batch
No exceptions or errors are logged. The server logs a normal clean disconnect and simply
never logs "Published will message" for this client, whereas it does log that line for the
`Dispose()`-without-DISCONNECT case using the same Will configuration.
```
### Code example
Minimal repro using only `MQTTnet` / `MQTTnet.Server` (no application code):
```csharp
using System.Net;
using System.Net.Sockets;
using MQTTnet;
using MQTTnet.Protocol;
using MQTTnet.Server;
int GetFreePort()
{
using var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
return ((IPEndPoint)listener.LocalEndpoint).Port;
}
var port = GetFreePort();
var serverFactory = new MqttServerFactory();
var server = serverFactory.CreateMqttServer(
serverFactory.CreateServerOptionsBuilder().WithDefaultEndpoint().WithDefaultEndpointPort(port).Build());
await server.StartAsync();
var clientFactory = new MqttClientFactory();
var receiver = clientFactory.CreateMqttClient();
var willReceived = false;
receiver.ApplicationMessageReceivedAsync += e =>
{
if (e.ApplicationMessage.Topic == "status") willReceived = true;
return Task.CompletedTask;
};
await receiver.ConnectAsync(new MqttClientOptionsBuilder().WithTcpServer("127.0.0.1", port).WithClientId("receiver").Build());
await receiver.SubscribeAsync(new MqttClientSubscribeOptionsBuilder()
.WithTopicFilter(f => f.WithTopic("status").WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)).Build());
var sender = clientFactory.CreateMqttClient();
await sender.ConnectAsync(new MqttClientOptionsBuilder()
.WithTcpServer("127.0.0.1", port)
.WithClientId("sender")
.WithWillTopic("status")
.WithWillPayload("DISCONNECTED")
.WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
// Explicitly asks the broker to deliver the Will even though this is a clean disconnect.
await sender.DisconnectAsync(MqttClientDisconnectOptionsReason.DisconnectWithWillMessage);
await Task.Delay(1000);
Console.WriteLine(willReceived ? "PASS: Will delivered" : "FAIL: Will NOT delivered");
// Actual: FAIL. Expected: PASS.
```
**Ideally a Unit Test (which shows the error) is provided so that the behavior can be reproduced easily.**
A `[TestMethod]` in the style of the existing `Source/MQTTnet.Tests/Server/Will_Tests.cs` (which already covers the "no Will on plain clean disconnect" case but has no coverage for the `DisconnectWithWillMessage` case this bug affects):
```csharp
[TestMethod]
public async Task Will_Message_Sent_On_DisconnectWithWillMessage()
{
using var testEnvironment = CreateTestEnvironment();
await testEnvironment.StartServer();
var receiver = await testEnvironment.ConnectClient();
var receivedMessages = testEnvironment.CreateApplicationMessageHandler(receiver);
await receiver.SubscribeAsync(new MqttTopicFilterBuilder().WithTopic("#").Build());
var clientOptions = new MqttClientOptionsBuilder().WithWillTopic("My/last/will");
var sender = await testEnvironment.ConnectClient(clientOptions);
// Client explicitly asks the broker to deliver the Will despite this being a clean disconnect.
await sender.DisconnectAsync(MqttClientDisconnectOptionsReason.DisconnectWithWillMessage);
await LongTestDelay();
// Currently FAILS: 0 messages received. Expected: 1 (the Will message).
Assert.HasCount(1, receivedMessages.ReceivedEventArgs);
}
```
Suggested fix in `Source/MQTTnet.Server/Internal/MqttConnectedClient.cs`:
```diff
- var isCleanDisconnect = DisconnectPacket != null;
+ var disconnectPacket = DisconnectPacket;
+ var suppressWill = disconnectPacket != null && disconnectPacket.ReasonCode != MqttDisconnectReasonCode.DisconnectWithWillMessage;
- if (!IsTakenOver && !isCleanDisconnect && Session.LatestConnectPacket.WillFlag && !Session.WillMessageSent)
+ if (!IsTakenOver && !suppressWill && Session.LatestConnectPacket.WillFlag && !Session.WillMessageSent)
```
This keeps the existing `Will_Message_Do_Not_Send_On_Clean_Disconnect` test passing (a plain `NormalDisconnection` still suppresses the Will) while fixing the `DisconnectWithWillMessage` case per MQTT 5.0 §3.14.2.1.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in Source/MQTTnet.Server/Internal/MqttConnectedClient.cs, especially RunAsync and its DisconnectPacket handling. Read Source/MQTTnet.Tests/Server/Will_Tests.cs and run the existing clean-disconnect test before adding coverage for DisconnectWithWillMessage. Done means the new case receives one Will message while the plain clean-disconnect case still receives none.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100