Skip to content

Chat WebSocket that dies while the tab is hidden is never detected: the visibility wake-up is gated on a flag only a detected disconnect sets #8273

Description

@DaBlitzStein

Summary

A chat WebSocket that dies while the browser tab is hidden is never detected, because the one listener written to recover that situation is guarded on a flag only a detected disconnect can set.

The user-visible result is a chat that spins forever on a message the daemon never received, and a reload that shows nothing — neither the question nor an answer, because the frame never left the machine.

Mechanism

All line numbers are crates/librefang-api/dashboard/src/pages/ChatPage.tsx at a394401b0.

#4063 added a visibilitychange / online listener so a user returning to the tab is not stuck on a dead socket until they refresh:

const wakeUp = () => {
  if (authErrorRef.current) return;
  if (!gaveUpRef.current) return;      // :302
  ...
  connect();
};

gaveUpRef.current = true is assigned in exactly one place — :263, inside ws.onclose, and only after retriesRef.current >= WS_MAX_RETRIES.

So the wake-up can only fire for a socket whose death the browser already noticed and retried to exhaustion.

A half-open socket never fires onclose — that is its defining property.
When the link dies while the tab is hidden (laptop suspend, wifi roaming to another access point, a NAT mapping expiring), the browser is never told.
No onclose, therefore no retries, therefore gaveUpRef stays false, therefore wakeUp returns at :302 having done nothing.

readyState still reports OPEN, which is what the send path branches on at :965, so the next message is written into a socket whose bytes go nowhere.
Nothing is persisted because nothing arrived, which is why a reload shows no trace of the message.

The intent of #4063 was right and the guard is what leaves it short: it covers the case where the browser noticed, and not the case where it could not.

Reproduction

  1. Open a chat and let the socket connect.
  2. Kill the link in a way that produces no FIN or RST — suspend the machine, or drop the connection at a middlebox — so the browser is never notified.
  3. Return to the tab and send a message.

Expected: the client notices the socket is unusable and reconnects, or at minimum does not silently swallow the message.

Actual: readyState is OPEN, the frame is written and lost, the chat shows a streaming bubble forever, and a reload shows neither the question nor an answer.

Note on what is already there

crates/librefang-api/src/ws.rs:1706 already answers {"type":"ping"} with {"type":"pong"}, and no client has ever sent it — zero occurrences across dashboard/src, and in Rust only the definition itself.
A fix does not need a protocol addition, only a caller.

Related

  • #3854 — the reconnect policy this listener belongs to, and #3930 which implemented it.
  • #4063 — added the visibilitychange / online wake-up whose guard is the subject here.
  • #8257 — a different path to a stuck turn (agent switch tears the socket down); it does not involve gaveUpRef or the visibility listeners.
  • Server side: neither end sends a periodic ping, so a dead peer is also undetectable from the daemon. Tracked separately.

Activity

  1. added 2 commits that reference this issue on Sep 10, 2026
    d5019f1
    9166db6
  2. added
    area/apiREST/WS endpoints and dashboard
    has-prA pull request has been linked to this issue
    on Sep 10, 2026
  3. added a commit that references this issue on Sep 13, 2026
    2b2725e
  4. added a commit that references this issue on Sep 13, 2026
    691b7ab
  5. removed
    has-prA pull request has been linked to this issue
    on Sep 13, 2026
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

    area/apiREST/WS endpoints and dashboard

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions