Server: DISCONNECT with DisconnectWithWillMessage reason code is ignored — Will message is not published

Open Beginner friendly
#2,256 0 comments 0 reactions 0 assignees View on GitHub

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

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

bug
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:

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
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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/MQTTnet

All issues in dotnet/MQTTnet

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.