TryGhost/Ghost · error
Download failed: ${response.status} ${response.statusText}
Error message
Download failed: ${response.status} ${response.statusText} What it means
A plain Error thrown by blobDownload() when the fetch returns a non-2xx status. The message embeds the HTTP status and statusText for diagnosis. Unlike the handleResponse pathway, blobDownload uses a raw fetch (helpers.ts:84) with no retry, no typed error class, and no explicit credentials option — so auth/cookie and proxy failures surface directly as this string error.
Source
Thrown at apps/admin-x-framework/src/utils/helpers.ts:87
return unquotedMatch[1].trim();
}
return undefined;
}
/**
* Downloads a file by fetching it as a blob and triggering a browser download.
* Use this instead of downloadFile/downloadFromEndpoint for streaming responses
* (e.g. large CSV exports) where the iframe approach may not work reliably.
*
* The filename comes from the response's `Content-Disposition` header;
* `fallbackFilename` is only used when the server omits it.
*/
export async function blobDownload(url: string, fallbackFilename?: string): Promise<void> {
const response = await fetch(url, {method: 'GET'});
if (!response.ok) {
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
}
const filename = getFilenameFromContentDisposition(response.headers.get('content-disposition'))
?? fallbackFilename
?? 'download';
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(blobUrl);
}
View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Inspect the embedded status: 401/403 → re-authenticate; 404 → verify the endpoint path; 5xx → check server/proxy.
- For cross-origin downloads, ensure credentials are sent (same-origin, or pass credentials:'include' if the helper is extended).
- Catch the error and surface the status to the user instead of a generic download-failed message.
Example fix
// before: opaque failure
try { await blobDownloadFromEndpoint('/members/download/'); }
catch (e) { alert('Download failed'); }
// after: parse the embedded status to guide the user
import {blobDownloadFromEndpoint} from '@tryghost/admin-x-framework/utils/helpers';
try { await blobDownloadFromEndpoint('/members/download/'); }
catch (e: any) {
const status = Number(e?.message?.match(/Download failed:\s*(\d+)/)?.[1]);
if (status === 401) { redirectToSignin(); return; }
alert(e.message);
} Defensive patterns
Strategy: try-catch
Validate before calling
// Confirm the endpoint and session are valid before streaming a download.
async function canDownload(url: string): Promise<boolean> {
const probe = await fetch(url, {method: 'HEAD', credentials: 'same-origin'});
return probe.ok;
} Try / catch
import {blobDownloadFromEndpoint} from '@tryghost/admin-x-framework/utils/helpers';
try {
await blobDownloadFromEndpoint('/members/download/');
} catch (e: any) {
const status = Number(e?.message?.match(/Download failed:\s*(\d+)/)?.[1]);
if (status === 401 || status === 403) {
redirectToSignin();
} else if (status) {
notify(`Download failed (${status}).`);
} else {
throw e;
}
} Prevention
- blobDownload uses a plain fetch with no explicit credentials option — for cross-origin downloads, ensure the session cookie is sent (same-origin) or extend it with credentials:'include'.
- Parse the embedded HTTP status from the message to drive UX (401 → re-auth, 404 → bad path, 5xx → server).
- For large exports, confirm the proxy won't time out streaming responses before delegating to blobDownload.
When it happens
Trigger: Calling blobDownload/blobDownloadFromEndpoint for a CSV export or other download whose URL returns non-2xx: 401 (session expired, cookie not sent), 403, 404, or a 5xx/gateway error. Because no credentials option is set, cross-origin downloads may lose the session cookie.
Common situations: Exporting members/posts as CSV after the session lapsed; cross-origin download URL where the cookie isn't sent by default; the export endpoint returns 404 after a route change; a proxy blocks the large streaming response.
Related errors
- Failed to fetch image: ${response.status}
- Request timed out, please try again.
- Failed to fetch changelog: ${response.status}
- Failed to fetch site data
- Failed to update member
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/163a38a122378560.
Report an issue: GitHub.