Skip to content

fix(errors): plain copy for failed history reads and connections - #3096

Draft
matt2e wants to merge 2 commits into
mainfrom
errors/user-copy-messages
Draft

matt2e wants to merge 2 commits into
mainfrom
errors/user-copy-messages

Conversation

@matt2e

@matt2e matt2e commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Failed history reads and failed community connections showed the relay's or browser's own text: exception names (ReadError:), HTTP statuses (Relay request failed (400)) and raw relay strings (rate-limited: quota exceeded; retry in 3600s). This adds one shared helper, userCopy(error, context), which turns the typed failures Buzz already produces into a plain sentence, a retry flag and raw details. It also adopts the helper on every history-read surface and on the connection failure of Workflows, Notes and the status-line pages.

This is the base of a stack. #3097 (uploads), #3098 (profile save) and #3099 (page and panel crashes) target this branch and reuse the helper.

What changed

Shared helper and typed failures

  • userCopy(error, context) (src/features/relay/user-copy.ts) classifies by type: ReadError kind and status, ApiPaused/ApiCapacity, UploadError, PublishRejected/PublishNotSent, fetch's network TypeErrors and DOMException names. It never parses text it doesn't own. text never carries an exception name, status, URL or raw string; those go in details.
  • ErrorDetails (src/shared/ErrorDetails.tsx) renders details under a collapsed Details disclosure. Only the page crash card in fix(pages): offer Try again and name the plugin when a page or panel crashes #3099 uses it. History and connection failures show no Details.
  • The relay service snapshot stores a typed RelayFailure ({ kind, details }) instead of String(error). Channel windows and thread snapshots keep a WindowFailure, and history parse failures are ReadError("invalid-response").
  • The window store marks errorSource: "older" only when loadOlder failed, so a head, catch-up or seek failure no longer reads as older history failing.
  • PublishNotSent carries the relay's wait (quota hint or held lane), so busy copy can say how long.
  • Copy and retry share one set of fetch network messages, including WebKit's. A body cut off after its headers is an unavailable read.
  • An answer this Buzz can't use (unusable, e.g. a 404 or 400 from an edge) is not retried at once. The service asks again in the background after 30 s, then 1, 2 and every 5 minutes. The failure stays on screen meanwhile.

History reads (ChannelBody, ChannelTimeline, NewMessage, ThreadPanel, MediaReviewViewer, DraftPreview)

Case Before After
Outage, 5xx, timeout Relay request failed (503) + Retry messages Messages didn’t load. + Try again
4xx (not 401/403) or invalid answer Relay request failed (400) + Retry messages Messages didn’t load. + Check for updates (no Try again)
Paused or full queue rate-limited: quota exceeded; retry in 3600s + Retry messages Messages didn’t load. Buzz is busy. Try again in about 1 hour. + Try again (… Try again in a few seconds. for a minute or less)
Refused, no known reason raw relay text Messages didn’t load. You don’t have access to this conversation.
Older-history edge Relay request failed (400) + Retry older messages Older messages didn’t load. + Try again or Check for updates
Drafts raw text, or Could not load conversation. + Retry conversation Messages didn’t load. + Try again or Check for updates
Thread older replies / media review raw text + Try again Older replies didn’t load. / This attachment didn’t load., with Try again only when retrying can help

Connection failures

Surface Before After
Workflows Connect to a community to browse workflows. / ReadError: Relay read failed (502) + Retry connection Card: Can’t connect / Check your connection. We’ll keep trying. + Try again
Notes Connect to a community to read notes. / ReadError: Relay read failed (502) + Retry connection The same card
Workflows, Notes, unusable answer (404) ReadError: Relay read failed (404) + Retry connection Can’t connect / Buzz couldn’t connect to this community. + Check for updates (Messages' card says the same)
Workflows/Notes, refused (403) ReadError: Relay read failed (403) + Retry connection You don’t have access to this community, no retry
Activity, its preview, Bestie, Pinned Bestie ReadError: Relay read failed (404) + Retry connection Buzz couldn’t connect to this community. + Check for updates; a failure that may clear says Check your connection. We’ll keep trying. + Retry connection
Reminders the raw error, or Could not connect., no button The same sentence and action as Activity

Connect to a community to … now shows only when there is genuinely no connection.

App page failure cards (App.tsx) put the quiet action first and the primary action last. Copy and visibility are unchanged, except that an unusable connection now gets the update sentence above and no Try again:

  • Can’t connect: Go to Messages, then Try again
  • <Plugin> is turned off: Go to Messages, then Manage plugins
  • This page isn’t available (removed or not found): Try again, then Go to Messages
  • This page isn’t available (didn’t load this time): Go to Messages, then Try again

Before / After

#alpha head read answered 400

Head read answered 400: Relay request failed (400) + Retry messages becomes Messages didn’t load. + Check for updates, because the same read fails every time. There is no Details.

#alpha head read answered 503

Head read answered 503: the same sentence, with Try again because a retry can help.

older page answered 400

Older page answered 400: Older messages didn’t load. with Check for updates in place of Retry older messages. (Round 1 capture; this copy is unchanged since.)

older page answered 503

Older page answered 503: Older messages didn’t load. with Try again. (Round 1 capture; this copy is unchanged since.)

Busy, head read answered 429 retry in 3600s

Busy, a 429 with "retry in 3600s": the relay's text becomes Buzz is busy. Try again in about 1 hour. Older capture (round 2): it still shows a Details disclosure under the button, which was removed later. The shipped history failure has no Details.

Workflows with a 502 session read

Workflows with a 502: the raw ReadError and "Connect to a community" are replaced by the Messages Can’t connect card with Try again.

Workflows with a 404 session read

Workflows with a 404, an answer this Buzz can't use: Buzz couldn’t connect to this community. with Check for updates instead of a retry that would fail the same way.

Notes with a 502 session read

Notes with the same 502: the same card instead of "Connect to a community to read notes." Older capture (round 1): it still shows a Details disclosure under the card, which was removed later. The shipped card has no Details.

Workflows when the community refuses the account

Workflows when the community refuses the account (403): the refusal title with no retry, instead of a raw 403 and Retry connection. (Round 1 capture; this state is unchanged since.)

Activity with a 404

Activity with a 404: the status line gets Check for updates instead of Retry connection. Older capture (round 2): it still reads "… Updating Buzz may fix this."; that sentence was dropped, and the shipped text is only Buzz couldn’t connect to this community.

Bestie with a 404

Bestie with a 404: the raw ReadError becomes the update sentence and Check for updates. Older capture (round 2): it still reads "… Updating Buzz may fix this."; the shipped text is only Buzz couldn’t connect to this community.

Checks

  • pnpm typecheck: clean on this branch.
  • pnpm lint: exit 0, with 30 infos that predate this change. biome format is clean on the changed files.
  • Tests were not run for this PR (the workstation was under load). The changed unit tests and browser specs (history-loading, live-status, new-message) are included but not run on this snapshot. Hosted CI is the first full run.
  • The four stacked branches merged together give exactly the tree of the source branch they were split from.

Known gaps

  • For an unusable answer, the app-level card on the Messages page has no button. Go to Messages is hidden there, and Check for updates was not added to that card.
  • "Couldn’t load channels." in the primary sidebar still offers Try again for every failure kind.

🤖 Generated with Claude Code

matt2e and others added 2 commits November 13, 2026 10:49
…the relay's words

Add userCopy(error, context) in features/relay. It classifies a failure
by the typed errors Buzz already produces (ReadError kind and status,
ApiPaused/ApiCapacity, UploadError, PublishRejected/PublishNotSent,
fetch's network TypeErrors, DOMException names) and returns { text,
retry, kind, details }. text is the site's own sentence plus a plain
next step and never carries an exception name, HTTP status, URL or raw
relay or native string; those stay in details. ErrorDetails renders
details under a collapsed "Details" disclosure.

The relay layer keeps failures typed instead of stringified:
- The service snapshot stores a RelayFailure ({ kind, details }) and
  channel windows and thread snapshots keep a WindowFailure. History
  parse failures are ReadError("invalid-response").
- The window store records errorSource "older" only when loadOlder
  failed, so a head or catch-up failure no longer blames older history.
- PublishNotSent carries the relay's wait for a quota hint or a held
  lane, so busy copy can say how long.
- Copy and retry share one set of fetch network messages (including
  WebKit's), and a body cut off after its headers is an unavailable read.
- An answer Buzz can't use is not retried at once. The service asks
  again in the background after 30 s, 1, 2 and every 5 minutes, keeping
  the failure on screen meanwhile.

History reads: ChannelBody, ChannelTimeline, NewMessage, ThreadPanel,
MediaReviewViewer and DraftPreview map the failure with historyFailure():
"Messages didn’t load." with "Try again" for an outage or 5xx; "Buzz is
busy. Try again in a few seconds." (or "in about N minutes/hours") when
paused; "Check for updates" and no Try again for a 4xx or invalid
answer; "You don’t have access to this conversation." for a refusal with
no known reason. The older edge says "Older messages didn’t load.".

Connection failures: Workflows and Notes show the Messages
ConnectionFailed card: "Can’t connect", "Check your connection. We’ll
keep trying." and "Try again".
An answer Buzz can't use reads "Buzz couldn’t connect to this
community." with "Check for updates"; a refusal shows the community
refusal title with no retry. Bestie, Pinned Bestie, Activity, its
preview and Reminders say the same through connectionFailureText and
connectionFailureAction. App's page failure cards put the primary
action last and drop Try again for an unusable connection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
Five journeys still asserted the copy that the previous commit replaced:

- thread-window: the older-replies alert said "Older page failed"; it now
  says "Older replies didn’t load." (ThreadPanel's OlderRepliesFailure).
- media-controls, message-overlays, gallery-theme: MediaReviewViewer's
  unavailable-media button was "Retry"; it is "Try again". The
  message-overlays title and the gallery comment follow the rename.
- navigation-repairs: the keyboard hand-off journey failed the session
  with an empty JSON object. That is now an answer Buzz can't use, so
  the card says "Buzz couldn’t connect to this community." and offers
  Check for updates, not Try again. The journey now fails the broker
  registration with a recorded 502 instead, a failure the service leaves
  for the user, so the card still says "Check your connection, then try
  again." and offers Try again. Every caller registers the community, so
  every registration fails until the user acts, and the retry is counted
  from then on.

Product copy is unchanged.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@matt2e
matt2e force-pushed the errors/user-copy-messages branch from 32c31dc to 0e35fbc Compare November 12, 2026 23:50

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant