Skip to content

fix: grant requested QoS in SUBACK instead of hard-coded QoS 0 - #6923

Open
wy471x wants to merge 9 commits into
apache:masterfrom
wy471x:fix_SUBACK-always-grants-QoS-0
Open

wy471x wants to merge 9 commits into
apache:masterfrom
wy471x:fix_SUBACK-always-grants-QoS-0

Conversation

@wy471x

@wy471x wy471x commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Make sure that:

  • You have read the contribution guidelines.
  • You submit test cases (unit or integration tests) that back your changes.
  • Your local test passed ./mvnw clean install -Dmaven.javadoc.skip=true.

Summary

Changes:

  • Subscribe.java: sendSubAckMessage now takes the filtered List and builds the SUBACK grant list from topicSub.qualityOfService().value() instead of
    hard-coding AT_MOST_ONCE for every topic (MQTT-3.8.4/3.9). FAILURE subscriptions remain excluded from the ack. Removed the now-unused MqttQoS and ArrayList imports.

Test Cases:

  • SubscribeTest.java (new): 3 tests — grants requested QoS (2/0/1), excludes FAILURE subscriptions, closes channel when already connected.

close #6848

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: #6923 honor requested QoS in SUBACK

Decision: APPROVE

What changed

  • Subscribe.sendSubAckMessage(...): previously the SUBACK always granted QoS 0 regardless of what the client requested. Now it grants each subscription the QoS the client requested (topicSub.qualityOfService().value()), which is the correct MQTT behavior.
  • The FAILURE-filtered subscription list is now collected into ackSubscriptions (kept before the topic-name mapping) and passed through to sendSubAckMessage.
  • Removed now-unused imports (java.util.ArrayList, io.netty.handler.codec.mqtt.MqttQoS).
  • Added SubscribeTest (Mockito + ArgumentCaptor) covering: granted QoS matches requested (2/0/1), FAILURE subscriptions are excluded from SUBACK and not looked up in topicRepository, and connected-state closes the channel without writing a SUBACK.

Verification

  • Granted QoS list length matches the filtered subscription count; FAILURE entries are dropped before SUBACK construction, consistent with prior behavior.
  • Honoring the requested QoS (server may grant equal-or-lower; here it grants exactly requested) is within MQTT spec and strictly better than always-0.
  • Tests assert grantedQoSLevels() ordering and exclusion, validating the logic.

Notes

  • None blocking. Behavior improvement, well-tested.

Approving.

@dengliming dengliming left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is still calling the old setConnected signature. MessageType#setConnected now requires the Channel plus the boolean flag, so this won’t compile in CI.

Suggested fix:

subscribe.setConnected(channel, true);

@Aias00

Aias00 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

CI note (PMC Aias00): the only failing check is CodeQL Analyze (java). Please review whether this is a genuine security finding introduced by the change or CodeQL noise, and address/confirm it before merge. The rest of the suite is green.

Aias00
Aias00 previously approved these changes Sep 24, 2026

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix — SUBACK was hard-coding QoS 0 regardless of what the subscriber asked for.

// before
for (int i = 0; i < ackTopics.size(); i++) {
    qos.add(MqttQoS.AT_MOST_ONCE.value());   // always 0
}

MQTT-3.8.4 requires the SUBACK payload to carry, for each subscription in order, the QoS level the server actually granted. Reporting 0 unconditionally tells a client that requested QoS 1 or 2 that it will get at-most-once delivery, so well-behaved clients downgrade their delivery guarantees (and stop sending PUBACK/PUBREC) even though the broker would have honoured the higher level. Carrying the requested qualityOfService().value() through is the right behaviour.

Also good: the filtered ackSubscriptions list is now what drives both the repository registration and the SUBACK, so the granted levels line up with the topics actually registered instead of being derived from a parallel list.

Tests are solid: testSubAckGrantsRequestedQoS asserts [2, 0, 1] for subscriptions requested in the order QoS2/QoS0/QoS1 — which also locks in that ordering is preserved, not just the set of values.

Two non-blocking observations:

  1. Failed subscriptions. MQTT-3.8.4-5 says a subscription that cannot be granted should get the failure code 0x80 in the SUBACK, keeping the response positionally aligned with the request. This PR filters FAILURE entries out of the list instead, so the SUBACK ends up shorter than the request. Clients that match by index will mis-align. Returning 0x80 for those entries would be spec-correct; testSubAckExcludesFailureSubscriptions currently documents the filtering behaviour, so changing it means updating that test.

  2. subscribe.setConnected(true) is used in testSubscribeWhenConnectedClosesChannel — if that setter is only intended for tests, a brief comment would help; if it's production API, it's fine as-is.

MessageType#setConnected is now keyed on the channel and Subscribe closes
the channel when it is not connected, so SubscribeTest was left stale by
the merged connection state guard and failed to compile.

Use EmbeddedChannel so the CONNECTED attribute and outbound writes are
real, and rename testSubscribeWhenConnectedClosesChannel to
testSubscribeBeforeConnectClosesChannel to assert the current guard.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning the requested QoS in SUBACK is directionally correct. Please confirm the broker actually supports every granted QoS and add coverage for false and legacy 0/1 representations so compatibility is explicit.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the current head. The previous test compilation issue is fixed and SUBACK now returns the requested QoS values using the current session API. The original blocker is resolved. The existing handling of rejected subscription entries remains a separate compatibility watch.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] SUBACK always grants QoS 0, ignoring the requested QoS

3 participants