Skip to content

Mobile: publish() silently drops the event when the socket is disconnected — "not acknowledged within 0:00:08" and the message is lost #5933

Description

@berkorbay

Summary

RelaySession.publish() sends with _socket?.send(...). When _socket is
null (or the underlying WebSocket has already died), the send is silently
skipped but the 8-second ack timer still runs
, so the user gets

TimeoutEvent <event id> not acknowledged within 0:00:08.000000

and the message is lost — no reconnect, no queue, no retry. The composed
text is gone from the outbox with only a transient toast to show for it.

I verified the loss server-side: the event id from the toast is not present
on the relay at all
(queried the channel history directly). It was never
transmitted.

https://github.com/block/buzz/blob/main/mobile/lib/shared/relay/relay_session.dart#L299-L323

  Future<NostrEvent> publish(
    NostrEvent event, {
    Duration timeout = const Duration(seconds: 8),
  }) {
    final completer = Completer<NostrEvent>();
    final timer = Timer(timeout, () { ...completeError(TimeoutException(...)) });
    _pendingEvents[event.id] = _PendingEvent(completer: completer, timeout: timer);

    _socket?.send(['EVENT', event.toJson()]);   // <-- no-ops when disconnected
    return completer.future;
  }

Why it triggers so easily on iOS

iOS suspends the app's socket as soon as it backgrounds, so the socket is
routinely dead when the user returns to the app. The relay independently
reaps the connection: heartbeat_loop pings every 30s and closes after 3
missed pongs (crates/buzz-relay/src/connection.rs), i.e. a ~90s ceiling.

Observed on my relay: the phone's connection is re-established every 40–90s
(WebSocket connection establishedNIP-42 auth successfulWebSocket connection closed, repeatedly). Any send issued in the window between the
socket dying and the client noticing hits the null-socket path.

Typing a message right after opening the app is therefore the normal case,
not an edge case.

Steps to reproduce

  1. Open the mobile app, let it connect to a channel.
  2. Background the app (or just leave it until the relay reaps the socket —
    under ~90s).
  3. Foreground it, immediately type a message and send.
  4. After 8 seconds: TimeoutEvent ... not acknowledged within 0:00:08.
  5. Query the relay for that event id — it does not exist. The message was
    never sent, and the composed text is not recoverable from the UI.

Expected

Any of, in rough order of preference:

  1. publish() awaits a live socket (reconnect + NIP-42 re-auth) before
    sending, rather than no-oping on null.
  2. Failing that, queue the event and flush on reconnect — mobile networks
    drop constantly; an outbox is the normal shape for this.
  3. At minimum: fail loudly and recoverably — keep the text in the composer,
    surface a retry affordance, and distinguish "never sent" from "sent but
    unacknowledged". Today those two are indistinguishable to the user, which
    matters because they need opposite responses (resend vs. don't).

Note that a bare timeout is also misleading: an 8s deadline on a message that
was never transmitted describes the symptom, not the failure.

Environment

  • Buzz iOS app, self-hosted relay (docker compose bundle) behind a reverse
    proxy, TLS, single community.
  • Relay and proxy verified healthy throughout: NIP-11 responds in ~24ms,
    no errors, no rate limiting, publishes from a CLI client accepted in ~40ms.
  • Two long-lived buzz-acp WebSocket clients on the same URL through the
    same proxy
    had zero reconnects in 3 days, so the connection churn is
    specific to the mobile client/platform, not the server path.
  • Phone and server on the same LAN, direct connection (no relay hop).

Related

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