Stirling-Tools/Stirling-PDF · error · Error

Download failed (${response.status})

Error message

Download failed (${response.status})

What it means

Thrown by downloadFromUrl() when fetch(url) resolves with response.ok === false. The HTTP status code is interpolated into the message. The blob is never read; this guards upstream errors (auth required, not found, server error) before attempting to save.

Source

Thrown at frontend/editor/src/desktop/services/downloadService.ts:43

    return { cancelled: true };
  }

  const result = await saveToLocalPath(request.data, savePath);
  if (!result.success) {
    throw new Error(result.error || "Failed to save file");
  }

  return { savedPath: savePath };
}

export async function downloadFromUrl(
  url: string,
  filename: string,
  localPath?: string,
): Promise<DownloadResult> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Download failed (${response.status})`);
  }
  const blob = await response.blob();
  return downloadFile({ data: blob, filename, localPath });
}

View on GitHub (pinned to 9ef20dcab8)

Solutions

  1. Parse the status from the message (or refactor to expose it) and handle 401/403 by refreshing the token, 404 by re-fetching a fresh link, 5xx by retrying.
  2. Ensure the request carries the needed Authorization header / cookies for protected resources.
  3. For signed URLs, generate a fresh one before retrying.
  4. Log the URL (without secrets) + status to identify the failing resource.

Example fix

// before
await downloadFromUrl(url, name);

// after: retry on transient 5xx, refresh auth on 401
try { await downloadFromUrl(url, name); }
catch (e) {
  const status = Number(/\((\d+)\)/.exec(e.message)?.[1]);
  if (status === 401) { await authService.refresh(); await downloadFromUrl(url, name); return; }
  if (status >= 500) { /* retry with backoff */ return; }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// sanity-check the URL before fetching
function isDownloadableUrl(u: string): boolean { try { const p = new URL(u); return p.protocol === 'http:' || p.protocol === 'https:'; } catch { return false; } }

Type guard

function isDownloadStatusError(e: unknown): e is Error {
  return e instanceof Error && /^Download failed \(\d+\)$/.test(e.message);
}

Try / catch

try { await downloadFromUrl(url, name); }
catch (e) {
  if (isDownloadStatusError(e)) {
    const status = Number(/\((\d+)\)/.exec(e.message)?.[1]);
    if (status === 401) { await authService.refresh(); return downloadFromUrl(url, name); }
    if (status >= 500) { /* retry with backoff */ return; }
  }
  throw e;
}

Prevention

When it happens

Trigger: Fetching the source URL returns 4xx/5xx: 401/403 when the resource needs auth the desktop didn't send, 404 for an expired/missing link, 500 from a failing backend, or a CDN/signed-URL that has expired.

Common situations: Expired presigned/S3 URL; backend endpoint that requires a JWT the caller didn't attach; tool output that was cleaned up server-side; wrong URL passed by the caller.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/6c827ad75a5b85c2. Report an issue: GitHub.