Skip to content

MQTT: anonymous-client disconnect fails will cleanup with AccessViolation ("Must login") on system.hdb_session_will #1874

Description

@harper-joseph

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 <sessionId> AccessViolation: Must login
    at authorizeActionOnResource (resources/Resource.ts)
    at hdb_session_will.applyContext (resources/Resource.ts)
    at <anonymous> (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 a0f4a51server/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:

// 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 authorizeActionOnResourceallowRead(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:

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

Filed by an agent (Claude Code) on behalf of @harper-joseph, from a live investigation.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Fields

    Priority

    P2

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions