jamiepine/voicebox · error · Error

HTTP ${res.status}

Error message

HTTP ${res.status}

What it means

Thrown by handleExportAudio() after fetching a capture's audio bytes from apiClient.getCaptureAudioUrl(selected.id). If the response is not ok, it throws a generic HTTP <status>. The function is wrapped in try/catch that routes failures to exportToastError(), so the user sees an error toast.

Source

Thrown at app/src/components/CapturesTab/CapturesTab.tsx:345

  const exportToastError = (err: unknown) => {
    toast({
      title: t('captures.toast.exportFailed'),
      description: err instanceof Error ? err.message : String(err),
      variant: 'destructive',
    });
  };

  const handleExportAudio = async () => {
    if (!selected) return;
    try {
      const dest = await save({
        defaultPath: `capture_${selected.id.slice(0, 8)}.wav`,
        filters: [{ name: 'Audio', extensions: ['wav'] }],
      });
      if (!dest) return;
      const res = await fetch(apiClient.getCaptureAudioUrl(selected.id));
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const buf = new Uint8Array(await res.arrayBuffer());
      await writeFile(dest, buf);
      exportToastSuccess(dest);
    } catch (err) {
      exportToastError(err);
    }
  };

  const handleExportTranscript = async () => {
    if (!selected) return;
    const text = (selected.transcript_refined || selected.transcript_raw || '').trim();
    if (!text) {
      toast({ title: t('captures.toast.exportEmpty'), variant: 'destructive' });
      return;
    }
    try {
      const dest = await save({
        defaultPath: `capture_${selected.id.slice(0, 8)}.txt`,

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Verify the capture has audio before enabling Export Audio (check a has_audio / audio_url field on CaptureResponse).
  2. Differentiate 404 (show 'audio no longer available') from other statuses in the toast.
  3. Confirm the backend capture-audio route and on-disk path are intact; check backend logs for the matching read error.
  4. Ensure the auth token is still valid (re-auth on 401).

Example fix

// before
const res = await fetch(apiClient.getCaptureAudioUrl(selected.id));
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// after
const res = await fetch(apiClient.getCaptureAudioUrl(selected.id));
if (!res.ok) {
  throw new Error(res.status === 404 ? 'Audio for this capture is no longer available.' : `HTTP ${res.status}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

function captureHasAudio(c: CaptureResponse): boolean {
  return Boolean(c.audio_url || c.has_audio);
}
// Gate export:
if (!captureHasAudio(selected)) return;

Try / catch

try {
  const res = await fetch(apiClient.getCaptureAudioUrl(selected.id));
  if (!res.ok) throw new Error(res.status === 404 ? 'Audio no longer available.' : `HTTP ${res.status}`);
  await writeFile(dest, new Uint8Array(await res.arrayBuffer()));
  exportToastSuccess(dest);
} catch (err) {
  exportToastError(err);
}

Prevention

When it happens

Trigger: GET on the capture-audio URL returns 404 (audio file missing on disk / never persisted), 500 (backend error reading the file), 401/403 (auth/session expired), or a gateway 502/504. Also fails if the backend is unreachable.

Common situations: Captures older than the audio retention window had their WAV pruned. The backend storage volume was wiped/moved. The capture recorded but audio write failed silently (transcript exists, audio does not).

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/ee415c5df967bd1f. Report an issue: GitHub.