HarperFast / HarperFast/harper
MQTT: anonymous-client disconnect fails will cleanup with AccessViolation ("Must login") on system.hdb_session_will
- Dominant language
- JavaScript
- Stars
- 89
- Forks
- 10
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 200
Description
## Summary
With `mqtt.requireAuthentication: false` and anonymous MQTT clients, an abrupt (non-clean) client disconnect makes Harper read the internal `system.hdb_session_will` table using the disconnecting client's own request context — which has `user: null` and `authorize: true`. The authorization check fails with `AccessViolation` ("Must login", statusCode 401):
```
[warn]: Error publishing MQTT will for AccessViolation: Must login
at authorizeActionOnResource (resources/Resource.ts)
at hdb_session_will.applyContext (resources/Resource.ts)
at (server/DurableSubscriptionsSession.ts)
at transaction (resources/transaction.ts)
at SubscriptionsSession.disconnect (server/DurableSubscriptionsSession.ts)
at WebSocket.onClose (server/mqtt.ts)
statusCode: 401
```
Under connect/disconnect churn (e.g. a reconnect storm after a restart), the transactions wrapping this cleanup pile up on `hdb_session_will` and get aborted:
```
[error]: Transaction was open too long and has been aborted after exceeding the open-transaction limit, from table: hdb_session_will/
```
## Affected versions
- Confirmed on harper-pro **5.1.22** (current latest stable).
- Present on `harper` `main` as of `a0f4a51` — `server/DurableSubscriptionsSession.ts`.
## Root cause
`SubscriptionsSession.disconnect()` builds its context from `createContext()`, which sets `user: this.user` and `authorize: true`, then uses that context to touch the internal will store:
```ts
// server/DurableSubscriptionsSession.ts
createContext(): any {
const context: any = { session: this, socket: this.socket, user: this.user, authorize: true };
...
}
disconnect(clientTerminated) {
const context = this.createContext();
transaction(context, async () => {
try {
if (!clientTerminated) {
const will = await getLastWill().get(this.sessionId); // authorized read of internal system.hdb_session_will
if (will) await publishMessage(will, will.data, context);
}
} finally {
await getLastWill().delete(this.sessionId); // same problem
}
}).catch((error) => { warn(`Error publishing MQTT will for ${this.sessionId}`, error); });
}
```
`getLastWill()` is the internal `system.hdb_session_will` table. Accessing it under a context with `authorize: true` and `user: null` (anonymous session) runs `authorizeActionOnResource` → `allowRead(null)` on a system table → `AccessViolation`. The failure is on the `.get()`, **before** any will is published, so it fires on every non-clean anonymous disconnect regardless of whether the client actually registered a will.
For contrast, the startup-recovery path in the same file handles this correctly — it reconstructs the stored user before publishing and does not run under an anonymous authorizing context:
```ts
for await (const will of getLastWill().search({})) {
const message = { ...will };
if (message.user?.username) message.user = await (server as any).getUser(message.user.username);
await publishMessage(message, data, message);
getLastWill().delete(will.id);
}
```
## Reproduction
1. Set `mqtt.requireAuthentication: false`.
2. Connect an anonymous MQTT client (with or without a will) over WS/TLS.
3. Drop the connection abruptly (non-clean disconnect).
4. Observe the `AccessViolation: Must login` warning. Under many concurrent disconnects, observe the `hdb_session_will` "transaction open too long" aborts.
## Impact
- Log noise proportional to anonymous non-clean disconnects; spikes during restart-driven reconnect storms.
- Anonymous clients' Last-Will messages are never delivered on abrupt disconnect.
- Transaction pressure on `system.hdb_session_will` during storms (the open-transaction-limit aborts).
Severity is low in steady state (the cleanup path only warns and the failed transaction unwinds), but it is constant noise for any deployment that permits anonymous MQTT, and it silently breaks will delivery for those clients.
## Suggested fix
Access the internal `hdb_session_will` store with an internal/system context (e.g. `authorize: false`, or a dedicated internal context) for the disconnect `get`/`delete`, rather than the client's authorizing context. When (re)publishing the will, reconstruct the stored user (as the startup-recovery path already does) so the publish is authorized as the will's owner rather than the live — possibly anonymous — session. The will-store bookkeeping is internal plumbing and shouldn't be gated on the disconnecting client's permissions.
## Related
- #1604 (MQTT duplicate-clientId session takeover; will-message + durable-session races) — same subsystem, distinct issue. The recent last-will persistence-race fix (`a0f4a51`) does not touch this authorization path.
Filed by an agent (Claude Code) on behalf of @harper-joseph, from a live investigation.
Contributor guide
Research direction
Start in server/DurableSubscriptionsSession.ts, focusing on SubscriptionsSession.disconnect(), createContext(), and the startup-recovery path that reads hdb_session_will. Reproduce with mqtt.requireAuthentication: false and an abrupt anonymous disconnect, then verify that will delivery and hdb_session_will cleanup complete without AccessViolation or transaction-abort warnings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, typescript
- Domain
- backend, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100