Skip to content

fix(dm): stop showing "Network error: cancelled" on message send - #8

Merged
Adron merged 1 commit into
devfrom
fix/dm-cancelled-network-error
Aug 2, 2026
Merged

fix(dm): stop showing "Network error: cancelled" on message send#8
Adron merged 1 commit into
devfrom
fix/dm-cancelled-network-error

Conversation

@Adron

@Adron Adron commented Aug 2, 2026

Copy link
Copy Markdown
Member

Problem

Sending a DM surfaced a red "Network error: cancelled" banner even though the message actually sent (see the reported screenshot). Root cause: a cancelled URLSession request (URLError(.cancelled), thrown when a SwiftUI .task is torn down on navigation/teardown) was flattened at the APIClient boundary into APIError.transport(message: "cancelled") and rendered as an error.

Two design defects turned a benign cancellation into a persistent banner:

  1. Cancellation treated as a real errorload()/send() catch every error and store it in the visible error; the .cancelled code was already lost at APIClient.swift:210.
  2. Sticky error — only a successful load()/send() cleared it; the poll loop that keeps the thread live swallowed errors and never cleared it.

Fixes

  • APIClient.performOnce — normalise URLError(.cancelled) / CancellationError to CancellationError instead of laundering it into a transport error. Genuine transport failures (offline, DNS, TLS, timeout) are unchanged.
  • View models (DMThreadViewModel.load()/send(), DirectMessagesListViewModel.load()/loadMore(), NewMessageViewModel.send()) — ignore CancellationError instead of pinning it to error.
  • DMThreadViewModel.pollOnce() — clear a stale error on a successful poll so a one-off cancellation self-heals.

Tests (all passing)

  • InterlinedKit APIClientTests — a cancelled request throws CancellationError, never APIError.transport; existing transport-error test still passes.
  • App DMThreadViewModelTests — cancelled load leaves error nil / not-loaded; cancelled send restores the draft without a banner; a successful poll clears a stale error.

Verified: InterlinedKit APIClientTests 11/11; App DMThreadViewModelTests 12/12, DirectMessagesListViewModelTests 11/11, NewMessageViewModelTests 8/8 — 0 failures.

🤖 Generated with Claude Code

A cancelled URLSession request (URLError.cancelled, thrown when a SwiftUI
.task is torn down on navigation/teardown) was flattened at the APIClient
boundary into APIError.transport(message: "cancelled") and rendered as a
red "Network error: cancelled" banner — even though the message actually
sent. The banner was also sticky: only a successful load/send cleared it,
while the poll loop that keeps the thread live swallowed errors and never
cleared it.

Fixes:
- APIClient.performOnce: normalise URLError(.cancelled)/CancellationError
  to CancellationError instead of laundering it into a transport error, so
  the cancellation signal survives the boundary. Genuine transport
  failures are unchanged.
- DMThreadViewModel.load()/send(), DirectMessagesListViewModel.load()/
  loadMore(), NewMessageViewModel.send(): ignore CancellationError instead
  of pinning it to the visible error.
- DMThreadViewModel.pollOnce(): clear a stale error on a successful poll so
  a one-off cancellation self-heals.

Tests:
- InterlinedKit APIClientTests: a cancelled request throws CancellationError,
  never APIError.transport.
- App DMThreadViewModelTests: cancelled load leaves error nil / not-loaded;
  cancelled send restores the draft without a banner; a successful poll
  clears a stale error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Adron
Adron merged commit 7e1a1cf into dev Aug 2, 2026
4 of 8 checks passed
@Adron
Adron deleted the fix/dm-cancelled-network-error branch August 2, 2026 18:01
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