Server: DISCONNECT with DisconnectWithWillMessage reason code is ignored — Will message is not published
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 84/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- csharp
- Domain
- backend, networking
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.
Description
Verification
- Not applicable — this is a protocol-correctness bug (a Will message that should be delivered is silently dropped), not a performance/memory/CPU issue.
- Not applicable, same reason as above.
- Reproduced with two independent, unmodified
MQTTnetclients talking to an in-processMQTTnet.Serverinstance — no application-specific wrapper code involved (repro below uses onlyMQTTnet/MQTTnet.ServerAPIs directly). - 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:
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:
- Using this version of MQTTnet
5.1.0.1559(also reproduces on latestmaster). - Run this code:
- Start an
MqttServeron a local port. - Connect client
receiver, subscribe to#. - Connect client
senderwith a Will registered (WithWillTopic/WithWillPayload). - Call
sender.DisconnectAsync(MqttClientDisconnectOptionsReason.DisconnectWithWillMessage).
- Start an
- With these arguments: any Will topic/payload; QoS 0 or 1, retain or not — reproduces regardless.
- See error:
receivernever gets the Will message. (For contrast, callingsender.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
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):
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):
[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:
- 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.
- Dominant language
- C#
- Stars
- 5.1k
- Forks
- 1.2k
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/MQTTnet
-
Difficulty 4/5 3-5 days Newbie friendliness 45/100
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
-
bug
Difficulty 3/5 1-2 days Newbie friendliness 55/100
-
Create version of MQTTnet.Extensions.ManagedClient that is compatible with version 5 of MQTTnet Openfeature-request
Difficulty 4/5 3-5 days Newbie friendliness 48/100
-
question
Difficulty 4/5 3-5 days Newbie friendliness 30/100
Similar issues
-
type/automation type/tech-debt
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
t/bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
ci-failure-cause test-failure
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
area:auth FE mvp P3
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
klasolsson81/jobbliggaren#1788 ·