{"record":{"id":"163a38a122378560","repo":"TryGhost/Ghost","slug":"download-failed-response-status-response-sta","errorCode":null,"errorMessage":"Download failed: ${response.status} ${response.statusText}","messagePattern":"Download failed: (.+?) (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/admin-x-framework/src/utils/helpers.ts","lineNumber":87,"sourceCode":"        return unquotedMatch[1].trim();\n    }\n\n    return undefined;\n}\n\n/**\n * Downloads a file by fetching it as a blob and triggering a browser download.\n * Use this instead of downloadFile/downloadFromEndpoint for streaming responses\n * (e.g. large CSV exports) where the iframe approach may not work reliably.\n *\n * The filename comes from the response's `Content-Disposition` header;\n * `fallbackFilename` is only used when the server omits it.\n */\nexport async function blobDownload(url: string, fallbackFilename?: string): Promise<void> {\n    const response = await fetch(url, {method: 'GET'});\n\n    if (!response.ok) {\n        throw new Error(`Download failed: ${response.status} ${response.statusText}`);\n    }\n\n    const filename = getFilenameFromContentDisposition(response.headers.get('content-disposition'))\n        ?? fallbackFilename\n        ?? 'download';\n\n    const blob = await response.blob();\n    const blobUrl = window.URL.createObjectURL(blob);\n    const a = document.createElement('a');\n\n    a.href = blobUrl;\n    a.download = filename;\n    document.body.appendChild(a);\n    a.click();\n    a.remove();\n    window.URL.revokeObjectURL(blobUrl);\n}\n","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/admin-x-framework/src/utils/helpers.ts#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: opaque failure\ntry { await blobDownloadFromEndpoint('/members/download/'); }\ncatch (e) { alert('Download failed'); }\n\n// after: parse the embedded status to guide the user\nimport {blobDownloadFromEndpoint} from '@tryghost/admin-x-framework/utils/helpers';\ntry { await blobDownloadFromEndpoint('/members/download/'); }\ncatch (e: any) {\n    const status = Number(e?.message?.match(/Download failed:\\s*(\\d+)/)?.[1]);\n    if (status === 401) { redirectToSignin(); return; }\n    alert(e.message);\n}","handlingStrategy":"try-catch","validationCode":"// Confirm the endpoint and session are valid before streaming a download.\nasync function canDownload(url: string): Promise<boolean> {\n    const probe = await fetch(url, {method: 'HEAD', credentials: 'same-origin'});\n    return probe.ok;\n}","typeGuard":null,"tryCatchPattern":"import {blobDownloadFromEndpoint} from '@tryghost/admin-x-framework/utils/helpers';\ntry {\n    await blobDownloadFromEndpoint('/members/download/');\n} catch (e: any) {\n    const status = Number(e?.message?.match(/Download failed:\\s*(\\d+)/)?.[1]);\n    if (status === 401 || status === 403) {\n        redirectToSignin();\n    } else if (status) {\n        notify(`Download failed (${status}).`);\n    } else {\n        throw e;\n    }\n}","preventionTips":["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."],"tags":["download","network","blob","csv-export","auth"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}