fix: unbounded 1-second retry loop on composer send #459

Open
opened 2026-07-25 23:35:25 +02:00 by dries · 0 comments
Owner

Code-quality audit (P2). frontend/src/components/assistant/Composer.tsx:564-586.

const runSend = async (text, imgs) => {
  sendingRef.current = true;
  setSending(true);
  while (mountedRef.current) {
    try {
      await onSendRef.current?.(text, imgs);
      ...
      break;
    } catch (err) {
      if (!(err instanceof BackendUnavailableError)) break;
      await new Promise(resolve => window.setTimeout(resolve, 1_000));
    }
  }
  ...
};

Why it matters

No attempt cap, no backoff, no user-visible cancel. The only exit is unmounting the component. With the backend down this is one request per second, indefinitely, from every open tab — and the composer stays locked (sending === true) the whole time, so the user cannot edit, cancel, or copy their message out. A tab left open overnight against a stopped backend is thousands of failed requests.

Suggested fix

Cap at ~5 attempts with exponential backoff (1s, 2s, 4s, 8s, 16s), then give up and route the message through the existing failedSends path so the user gets it back and can retry deliberately.

Acceptance criteria

  • Retries are bounded and backed off.
  • After exhaustion the composer unlocks and the message surfaces via failedSends.
  • Test: onSend always throws BackendUnavailableError — assert a bounded call count and that sending returns to false.

Effort: S.

Code-quality audit (P2). `frontend/src/components/assistant/Composer.tsx:564-586`. ```js const runSend = async (text, imgs) => { sendingRef.current = true; setSending(true); while (mountedRef.current) { try { await onSendRef.current?.(text, imgs); ... break; } catch (err) { if (!(err instanceof BackendUnavailableError)) break; await new Promise(resolve => window.setTimeout(resolve, 1_000)); } } ... }; ``` ## Why it matters No attempt cap, no backoff, no user-visible cancel. The only exit is unmounting the component. With the backend down this is one request per second, indefinitely, from every open tab — and the composer stays locked (`sending === true`) the whole time, so the user cannot edit, cancel, or copy their message out. A tab left open overnight against a stopped backend is thousands of failed requests. ## Suggested fix Cap at ~5 attempts with exponential backoff (1s, 2s, 4s, 8s, 16s), then give up and route the message through the existing `failedSends` path so the user gets it back and can retry deliberately. ## Acceptance criteria - [ ] Retries are bounded and backed off. - [ ] After exhaustion the composer unlocks and the message surfaces via `failedSends`. - [ ] Test: `onSend` always throws `BackendUnavailableError` — assert a bounded call count and that `sending` returns to `false`. Effort: S.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dries/ocman#459
No description provided.