Skip to content

publish:failed not emitted with relay's OK,false reason when relay rejects with auth-required: after authentication has already succeeded #389

Description

@xeruf

Version: @nostr-dev-kit/ndk@3.0.3

Summary

When a relay rejects a published event with OK,false,"auth-required: …" after the client has already authenticated, NDK silently holds the publish resolver waiting for a retry-after-auth that never happens, and the publish promise rejects only when the internal 2.5s timer fires — losing the actual rejection reason.

Reproduction

  1. Connect to a NIP-42 relay with default auth policy. Authenticate successfully (relay:authed fires).
  2. Publish an event with a pubkey the relay does not allow (e.g. not in its whitelist).
  3. Relay returns ["OK","",false,"auth-required: pubkey not in whitelist"].

Expected

relay.publish() rejects with new Error("auth-required: pubkey not in whitelist"), and relay.emit("publish:failed", event, err) carries that error so listeners can react to permanent denials.

Actual

  • NDKRelayConnectivity (src/relay/connectivity.ts:451-471) inspects the reason and matches the substring "auth-required". Because pendingAuthPublishes.get(id) still has the event, it pushes the resolver back onto openEventPublishes and waits for retryPendingAuthPublishes() to be called.
  • retryPendingAuthPublishes() is only invoked from the auth() success path (connectivity.ts:1170). Since auth already succeeded earlier, it is never called again.
  • The resolver hangs until NDKRelayPublisher.publish's 2.5s Promise.race timer fires (src/relay/publisher.ts:43-48, 76).
  • publish:failed is emitted with Error("Timeout: 2500ms"), not the original reason.

The original OK,false reason is unrecoverable through any public NDK signal in this case.

Root cause

The isAuthRequired branch in connectivity.ts assumes that an auth-required reason means "we haven't authenticated yet, retry after auth". It does not consider that:

(a) the client may already be AUTHENTICATED, so no retry can happen, and
(b) auth-required: pubkey not in whitelist (or blocked: not authorized, etc.) is a permanent denial even when authenticated, not a retryable challenge.

Suggested fix

In the isAuthRequired branch of connectivity.ts, only hold the resolver if this._status < NDKRelayStatus.AUTHENTICATED. If already authenticated, treat the rejection as terminal and firstEp.reject(new Error(reason)) immediately so publish:failed carries the actual reason.

Optionally, the substring match could also be tightened to distinguish retryable challenges (e.g. bare "auth-required") from permanent denials ("pubkey not in whitelist", "blocked", etc.) regardless of auth status.

Impact

Consumers cannot reliably detect read-only / write-restricted relays from publish:failed, and have to fall back to parsing raw WebSocket frames via relay.connectivity.ws (private API) to get the rejection reason.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions