{"record":{"id":"7803d932a5ce6bf1","repo":"Wei-Shaw/sub2api","slug":"admin-backup-actions-downloadfailed","errorCode":null,"errorMessage":"admin.backup.actions.downloadFailed","messagePattern":"admin\\.backup\\.actions\\.downloadFailed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"frontend/src/views/admin/BackupView.vue","lineNumber":772,"sourceCode":"    if (error?.response?.status === 409) {\n      appStore.showWarning(t('admin.backup.operations.alreadyInProgress'))\n    } else {\n      appStore.showError(error?.message || t('errors.networkError'))\n    }\n    creatingBackup.value = false\n  }\n}\n\nasync function downloadBackup(id: string) {\n  try {\n    const result = await backupStepUp.run(() => adminAPI.backup.getDownloadURL(id))\n    if (result.parts && result.parts.length > 0) {\n      downloadParts.value = result.parts\n      downloadPartsModalOpen.value = true\n      return\n    }\n    if (!result.url) {\n      throw new Error(t('admin.backup.actions.downloadFailed'))\n    }\n    // 预签名 URL 带 attachment disposition，同页 anchor 导航直接触发下载；\n    // 不用 window.open：step-up 弹窗 await 会耗尽瞬态用户激活，新标签页会被浏览器拦截。\n    const link = document.createElement('a')\n    link.href = result.url\n    link.rel = 'noopener'\n    link.click()\n  } catch (error) {\n    if (isStepUpCancelled(error)) return\n    if (reportStepUpBlocked(error)) return\n    appStore.showError((error as { message?: string })?.message || t('errors.networkError'))\n  }\n}\n\nfunction closeDownloadParts() {\n  downloadPartsModalOpen.value = false\n  downloadParts.value = []\n}","sourceCodeStart":754,"sourceCodeEnd":790,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/frontend/src/views/admin/BackupView.vue#L754-L790","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nif (!result.url) {\n  throw new Error(t('admin.backup.actions.downloadFailed'))\n}\n\n// after\nif (!result.url && !(result.parts && result.parts.length > 0)) {\n  throw new Error(t('admin.backup.actions.downloadFailed') + ` (backup ${id}, no url/parts in response)`)\n}\nif (!result.url) {\n  throw new Error(t('admin.backup.actions.downloadFailed'))\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"interface DownloadResult { url?: string; parts?: string[] }\nfunction hasDownloadTarget(r: DownloadResult): r is { url: string } | { parts: string[] } {\n  return (typeof r.url === 'string' && r.url.length > 0) || (Array.isArray(r.parts) && r.parts.length > 0);\n}","tryCatchPattern":"try { await downloadBackup(id); }\ncatch (e) {\n  if (e.message === t('admin.backup.actions.downloadFailed')) {\n    showError('Backup file missing in storage — regenerate the backup'); return;\n  }\n  showError(e.message);\n}","preventionTips":["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"],"tags":["backup","presigned-url","object-storage","admin","frontend"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}