Repository navigation
MQTT: send each client the CONNACK refusal code of its own protocol version - #3402
Conversation
Release cherry-pick
|
There was a problem hiding this comment.
Code Review
This pull request updates the MQTT server implementation to correctly return protocol-specific connection refusal codes (reason codes for MQTT v5 and return codes for MQTT v3.1.1) during connection establishment (CONNACK). It introduces helper utilities to map internal errors to appropriate protocol codes and adds comprehensive integration and unit tests to verify these behaviors. There are no review comments, and I have no feedback to provide.
|
Suggestion: add a read-only-node arm to the CONNACK test. On main The residual behavior, durable sessions ( — Claude Opus 5.5 (finding triage) |
…rsion mqtt-packet writes reasonCode to a v5 client and returnCode to a 3.1.1 client, but the refusal sites had the two swapped, so a v5 client got the 3.1.1 code and the reverse. Bad credentials are now 0x86 / 4, not authorized 0x87 / 5, unspecified 0x80 / 5. An authorizeClient error whose numeric code is an HTTP status is mapped to Not authorized instead of being written to the wire; a code is passed on only if it is a refusal code in the client's own version. Refs #3298 Dispatch-Task: harper-mqtt-connack-reason-codes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeYyDXfTjDKHwpzUXyTHDZ
…d pass on only CONNACK-legal v5 codes Dispatch-Task: harper-mqtt-connack-reason-codes Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeYyDXfTjDKHwpzUXyTHDZ
…r fault A hook refuses a CONNECT by throwing, usually a plain Error. That reached a 3.1.1 client as return code 3 (server unavailable), which clients retry, for what is a permanent denial. The hook now has its own catch that defaults to Not authorized; storage and takeover failures keep the unspecified code. Dispatch-Task: pr-maint-3d73ce37e83a10641219417272e78bdf Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
c39d665 to
50111f6
Compare
Apply the resources/DESIGN.md prose-only trim from main commit 489eebc (#3429), reducing the note from 1014 to 998 lines so the design-doc gate passes on this PR without rebasing or changing runtime behavior. Dispatch-Task: pr-maint-3d73ce37e83a10641219417272e78bdf Co-Authored-By: GPT-5 Codex <noreply@openai.com>
⊙ Problem
mqtt-packet sends
reasonCodeto a v5 client andreturnCodeto a 3.1.1 client. The CONNACK refusals inserver/mqtt.tshad the two swapped, so a v5 client with a wrong password was told4and a 3.1.1 client0x86. Anerror.codethrown during connect, such as an HTTP status, was also written as is. #3303 fixed SUBACK, PUBACK and DISCONNECT, not CONNACK. Refs #3298.🤔 Is this the right problem?
Yes. Clients act on this code: a v5 client that gets
4cannot tell bad credentials from a protocol error.💡 Solution
CONNACK_REFUSALS): bad credentials0x86/4, not authorized0x87/5, anything else0x80/3.authorizeClient(the usual way a hook refuses) defaults to "not authorized" (its own catch); other connect failures are "unspecified".error.codeis sent as is only if the client's version allows it in a CONNACK (connackRefusalFor). Otherwise 401 or 403 (statusCodeorcode) is "not authorized", and the rest takes the default. The refusal sites now use these (bad credentials, session errors).🤔 Unspecified is
3for 3.1.13.1.1 has no generic refusal. For a session or storage failure,
5would suggest a credentials problem.3("server unavailable") is what a session takeover timeout already sends. A hook refusal is a denial, so it keeps5.🤔
error.mqttReasonCodeis not read at CONNACKOnly the inbound QoS 2 path sets it, and never during CONNECT. Reading it too would add a second way to name a code with no caller.
✅ Verification
evictionBlobs.test.js:164, also failing on main. Node 24 additionally failstransaction.test.js:507, the same additional-audit-ref assertion present in main's Node 26 job. All job logs are now available; these failures are outside this diff.runLinterandFormat Checkpassed oned89e94ce.npm run lint:required,npm run check:design-docs, and changed-file Prettier pass oned89e94ce. The upstream design-note trim from merged Adopt rocksdb-js getRegistryStatus() in place of the deprecated registryStatus() alias #3429 restores the document to 998 lines. This maintenance commit leaves runtime code unchanged.MQTT implementation validation from the authoring and rebase runs
mqttNotAuthorized.test.js. 17 fail onorigin/main(built side by side), all CONNACK cases, and all pass here.mqtt-not-authorized.test.tsover a real WebSocket client, using a fixture hook that refuses client ids startingdenied-with a 403 (config); all 4 fail onorigin/main(actual: 4, expected: 134).integrationTests/mqtt/**: 59 pass.unitTests/server/**: 1451 pass before the rebase; the three MQTT unit files pass after it.tsc, oxlint and prettier are clean.test:unit:mainhit its 50 minute limit on this host before finishing. The 47 failures it had logged are in deploy-activation andEntryHandlersuites, none touching MQTT. The MQTT unit files and the rest ofunitTests/server/**pass.PR QA: skipped @ ed89e94 — documentation-only maintenance.
🤔 Why include the design-doc trim?
It is identical to merged main #3429. On this branch the linter fails at 1,014 lines. The separate trim commit fixes CI and keeps the PR's history. The 1,000-line gate remains intact.
⚖️ Alternatives
🤔 Keep the existing hook policy
Numeric hook codes stay version-specific, as the explicit tests require. Translating them across versions would change that policy. The cost of one refusal closure per CONNECT is unmeasured. This lint repair keeps that existing behavior. Post-hook 401/403 coverage and comment cleanup remain follow-ups. The async-hook and socket-close deferrals below remain separate.
🤔 Keep the published CONNECT handler
A single CONNECT catch with a hook-stage fallback could replace the shared handler. Both can preserve refusal semantics. This maintenance commit only restores the doc budget, so it retains the published runtime implementation.
🤖 Generated by Anthropic Claude and OpenAI Codex; posted via @kriszyp.
https://claude.ai/code/session_01BeYyDXfTjDKHwpzUXyTHDZ
Release target: v5.3 — fixes code from #3303, a backport to v5.3
Related PRs: #3446 overlaps, #3464 overlaps, #3429 overlaps, #3430 overlaps, #3221 overlaps, #3456 overlaps, #3423 overlaps, 5 others independent
Complexity: medium
Dispatch: task
harper-mqtt-connack-reason-codes· queued by unknown · ran by claude/sonnet/high · worker kzyp-xps-1Deferred findings
b9c349471e41a5f6minor security —server/mqtt.ts— Async authorizeClient rejection can admit CONNECT — NOT FILED55167bb87455efd4minor correctness —server/mqtt.ts— Refusal CONNACK leaves the connection open — NOT FILEDReview-Coverage: authored=codex; ran=gemini,claude,cursor-grok; adjudicated=domain; declined=cursor-composer,muse; rounds=6; full=2 @ ed89e94
Review-Attention: study ~9m (hot: mqtt.ts; decisions: hook-fault-policy, do-less-alternative, bare-hook-error-is-denial, generic-legacy-refusal) @ ed89e94