Skip to content

fix(relay): show the relay's reason when it refuses a write - #2031

Merged
loganj merged 3 commits into
mainfrom
larry/show-relay-refusal-reason
Nov 4, 2026
Merged

loganj merged 3 commits into
mainfrom
larry/show-relay-refusal-reason

Conversation

@loganj

@loganj loganj commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

When the relay refuses a write, buzz-app showed Relay rejected the message (400) or, on the web broker path, Relay request failed (503). The relay's own reason never reached the user. For example, the relay refuses to create an open channel with a taken name (block/buzz#8782) with invalid: an open channel named "Launch" already exists. The create dialog showed only a status summary.

Now the create dialog shows An open channel named "Launch" already exists, on the signed path (packaged and native builds) and on the development broker path (just desktop / just web with BUZZ_DEV_VIEWER). On the signed path this holds for every relay-shaped refusal. On the broker path it holds for validation (invalid:) refusals only; restricted: and blocked: still show a status summary there, as before this PR.

Details

  • acceptPublish (src/features/relay/transport.ts)
    • For 400, 401, 403, 404, 413 and 422, it builds the display message from {error} text, but only when the text is relay-shaped, that is, it starts with a NIP-01 prefix (invalid, blocked, restricted, error, pow, duplicate). It removes the prefix and capitalizes the first letter.
    • Text without a prefix (for example, a gateway or VPN edge answering 403 {"error": "Forbidden by proxy"}) still shows Relay rejected the message (<status>). The prefix is a shape check, not proof of origin.
    • PublishRejected.reason keeps the raw text, so writers that classify by prefix (agent-config-artifacts.ts) work as before.
    • Text with a prefix that writers match on (conflict:, rate-limited:, auth-required:) is shown exactly as sent.
    • The 409 branch and the rate-limit handling are unchanged. A 200 receipt with accepted:false is unchanged; the name refusal comes as an HTTP 400, not on that path.
  • Signed transport (src/features/relay/http-admission.ts)
    • Admission reduced every upstream 4xx body to Relay request failed (400) before acceptPublish saw it.
    • The signed publish now uses the existing caller-allowed reason hook to keep relay-shaped {error} text for the refusal statuses above. readApiFailure still bounds it. The hook's callback now also receives the HTTP status; existing callers are unaffected.
  • Development broker (browser-host/relay-broker.mjs, src/features/relay/socket-requests.ts)
    • The broker publishes over WebSocket and answered every proven refusal with 503 Socket publication could not be confirmed.
    • A proven invalid: refusal now answers 400 with the relay's text (bounded at 16 KB by the socket receipt check), as the relay's HTTP API does. So both paths reach the same acceptPublish branch.
    • The broker log still gets only the safe summary (publicationRefusal), never the relay's text.
    • restricted:, blocked:, rate-limited:, conflict:, the read-state timestamp refusal and unconfirmed outcomes keep their current answers.

Testing

  • transport.test.ts:
    • Each prefix is removed. Text that writers match on is kept as sent.
    • Text without a prefix, and missing or invalid bodies, fall back to the status summary on both the signed and broker transports.
    • The 409 branch is unchanged, and a 500 stays a status summary.
  • relay-broker-live.test.mjs (the real broker, with a scripted relay socket):
    • A taken-name negative OK reaches the writer as PublishRejected with An open channel named "Launch" already exists.
    • The diagnostics test now expects 400 with the relay's text for invalid: refusals, and still checks that the log line holds only the safe summary and no private text.
    • With the broker change removed, these 3 cases fail.
  • native.test.ts: one fixture now uses relay-shaped text.
  • tsc --noEmit and biome check pass. vitest run browser-host src/features/relay src/features/developer src/features/agents src/features/channel-members src/bundled/channels src/bundled/workflows passes (5,432 tests) after the rebase onto main.
  • Not done: a run against a live relay. buzz-app accepts only wss:// and https:// relay origins, and both dev paths use the developer's own account credential.

🤖 Generated with Claude Code

@loganj
loganj force-pushed the larry/show-relay-refusal-reason branch from 3d805eb to 0075cf4 Compare November 2, 2026 16:48
@loganj
loganj marked this pull request as ready for review November 2, 2026 17:02
@loganj
loganj requested a review from a team as a code owner November 2, 2026 17:02
loganj and others added 3 commits November 2, 2026 12:49
A write the relay refused over HTTP (400, 401, 403, 404, 413, 422) showed
"Relay rejected the message (400)". The relay's own text, for example
`invalid: an open channel named "Launch" already exists`, was kept only
in PublishRejected.reason, and on the signed transport admission reduced
it to a status summary before acceptPublish saw it.

- acceptPublish now shows the relay's text without its NIP-01 prefix
  (invalid, blocked, restricted, error, pow, duplicate), for example
  `An open channel named "Launch" already exists`. `reason` keeps the
  raw text for writers that classify it. Text with another prefix
  (conflict:, rate-limited:) is shown as sent. With no text, the status
  summary stays.
- The signed publish asks admission to keep the relay's {error} text,
  only for those refusal statuses, through the existing caller-allowed
  reason hook. readApiFailure still bounds it.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
A gateway or VPN edge can answer 400-422 with JSON {error}, and that
text was shown as if the relay had refused. Show refusal text only when
it starts with a NIP-01 prefix, and let admission keep only that text on
the signed path. Otherwise show the status summary, the same on both
publish paths. `reason` still holds the raw text for classification.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
The dev broker publishes over WebSocket and answered every proven refusal
with 503 "Socket publication could not be confirmed", so `just desktop`
still showed "Relay request failed (503)" for a taken channel name. A
proven `invalid:` refusal now answers 400 with the relay's bounded text,
as the relay's HTTP API does, so the broker and signed paths show the
same reason. Logs keep only the safe summary.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj force-pushed the larry/show-relay-refusal-reason branch from 0075cf4 to 2caec1d Compare November 2, 2026 17:50

@kazemek21 kazemek21 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kyle reviewed this PR and the Nexus review-tracker notes and approved with no comments.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account.

No actionable findings in the reviewed change. The signed path preserves bounded, relay-shaped refusal text through admission; the broker exposes proven invalid refusals to the requesting client while retaining sanitized diagnostic summaries. Raw reasons remain available to classifying writers. Conflict, quota and uncertain-delivery handling remain distinct.

Reviewed head: 2caec1d
Base: 6f341ce

Independent Rocket, Mantis and Groot lanes completed, with Star Lord integration. Source-only review of the seven-file diff, supporting transport/outbox/create-channel callers, tests and public PR material. No tests, builds, apps or live-relay workflows were run, and CI was not assessed. Actual error/retry focus, native/browser rendering and human acceptance remain unverified. This COMMENT is not approval or merge authorization.

@loganj
loganj merged commit 1d9f64b into main Nov 4, 2026
76 of 79 checks passed
@loganj
loganj deleted the larry/show-relay-refusal-reason branch November 4, 2026 01:31
tlongwell-block added a commit that referenced this pull request Nov 4, 2026
* origin/main: (26 commits)
  fix(identity): read FI backup status through the native forwarder (#2344)
  fix(relay): show the relay's reason when it refuses a write (#2031)
  perf(relay): look up archived identities in a per-snapshot set (#2359)
  fix(messages): keep link previews clear of message hover actions (#2351)
  fix(security): close workflow, Canvas, upload, and broker gaps (#2360)
  fix: prevent false leave failures after membership updates (#2310)
  feat(bestie): remove your Bestie, and let remote agents be Bestie after a warning (#2041)
  feat(messages): open the sender's profile from the byline name (#2253)
  feat(native-mcp): one-command launcher and MCP registration for agent driving (#2272)
  perf(composer): stop per-keystroke chip re-renders and reparses in long rich drafts (#2251)
  fix(agents): prevent history from forging prompt requests (#2223)
  Enable automatic key backup in preview releases (#2325)
  feat(read-state): n-blob NIP-RS sync with a 30-day floor, frontier only (#1286)
  feat(huddles): provide read-only huddleStatus for optional consumers (#2327)
  feat(search): show verified names in people search (#2322)
  Fix voice-note sending and restore waveform previews (#2283)
  Preserve Solo queued messages across restarts (#1704)
  feat(plugins): resolve attachment renderers by order, like panels and links (#2284)
  fix(channels): keep muted DM names from becoming bold (#2184)
  fix: command-click opening desktop message links in the browser (#2288)
  ...

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Tyler Longwell <tlongwell@squareup.com>
Signed-off-by: Tyler Longwell <tlongwell@squareup.com>

# Conflicts:
#	src/bundled/huddles/index.tsx
#	src/features/huddle/service.test.ts
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.

3 participants