block / block/buzz

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

Open
#5,933 3 comments 1 reaction 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## 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 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

```dart
Future publish(
NostrEvent event, {
Duration timeout = const Duration(seconds: 8),
}) {
final completer = Completer();
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 established` → `NIP-42 auth successful` → `WebSocket
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

- #3975 (desktop fails to re-establish a dropped relay socket) looks like the
same class of problem on the other client — a dead socket that the send
path doesn't account for.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.