Wei-Shaw/sub2api · error
admin.backup.actions.downloadFailed
Error message
admin.backup.actions.downloadFailed
What it means
In frontend/src/views/admin/BackupView.vue:772, downloadBackup() calls backupStepUp.run(() => adminAPI.backup.getDownloadURL(id)) and expects either a multi-part list (result.parts) or a single presigned URL (result.url). If the response has neither, it throws the localized 'admin.backup.actions.downloadFailed'. The presigned URL is then triggered via a synthetic anchor click to preserve transient user activation.
Source
Thrown at frontend/src/views/admin/BackupView.vue:772
if (error?.response?.status === 409) {
appStore.showWarning(t('admin.backup.operations.alreadyInProgress'))
} else {
appStore.showError(error?.message || t('errors.networkError'))
}
creatingBackup.value = false
}
}
async function downloadBackup(id: string) {
try {
const result = await backupStepUp.run(() => adminAPI.backup.getDownloadURL(id))
if (result.parts && result.parts.length > 0) {
downloadParts.value = result.parts
downloadPartsModalOpen.value = true
return
}
if (!result.url) {
throw new Error(t('admin.backup.actions.downloadFailed'))
}
// 预签名 URL 带 attachment disposition,同页 anchor 导航直接触发下载;
// 不用 window.open:step-up 弹窗 await 会耗尽瞬态用户激活,新标签页会被浏览器拦截。
const link = document.createElement('a')
link.href = result.url
link.rel = 'noopener'
link.click()
} catch (error) {
if (isStepUpCancelled(error)) return
if (reportStepUpBlocked(error)) return
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
}
}
function closeDownloadParts() {
downloadPartsModalOpen.value = false
downloadParts.value = []
}View on GitHub (pinned to 073e92d171)
Solutions
- Inspect the raw getDownloadURL response in devtools for the backup id to see the actual shape and fields.
- If the object is missing in storage (expired lifecycle rule), regenerate the backup before download and align storage retention with DB retention.
- Verify the storage credentials have permission to generate presigned GET URLs (s3:GetObject).
- Treat empty-parts+no-url as a server-side defect: log it distinctly rather than showing only the generic downloadFailed toast.
Example fix
// before
if (!result.url) {
throw new Error(t('admin.backup.actions.downloadFailed'))
}
// after
if (!result.url && !(result.parts && result.parts.length > 0)) {
throw new Error(t('admin.backup.actions.downloadFailed') + ` (backup ${id}, no url/parts in response)`)
}
if (!result.url) {
throw new Error(t('admin.backup.actions.downloadFailed'))
} Defensive patterns
Strategy: type-guard
Type guard
interface DownloadResult { url?: string; parts?: string[] }
function hasDownloadTarget(r: DownloadResult): r is { url: string } | { parts: string[] } {
return (typeof r.url === 'string' && r.url.length > 0) || (Array.isArray(r.parts) && r.parts.length > 0);
} Try / catch
try { await downloadBackup(id); }
catch (e) {
if (e.message === t('admin.backup.actions.downloadFailed')) {
showError('Backup file missing in storage — regenerate the backup'); return;
}
showError(e.message);
} Prevention
- Type-guard the download response shape before acting on it
- Align storage lifecycle retention with backup DB retention so objects never vanish while rows exist
- Verify storage credentials can presign GETs before enabling backup downloads
When it happens
Trigger: getDownloadURL(id) succeeds (2xx) but returns an object with no url and empty/missing parts — e.g., backend backup record exists but the object storage presign failed silently, the backup file was already expired/deleted from storage, or an API version mismatch where the field is named differently (download_url).
Common situations: S3-compatible storage lifecycle rules deleting old backup objects while DB rows remain; MinIO/OSS credentials valid for listing but not presigning; backend upgraded and response shape changed without updating the admin frontend.
Related errors
- HTTP error! status: ${response.status}
- admin.accounts.grok.noResponseBody
- Passkeys are not supported by this browser
- Passkey sign-in was cancelled
- Passkey creation was cancelled
AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15).
Data as JSON: /api/errors/7803d932a5ce6bf1.
Report an issue: GitHub.