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

  1. Inspect the raw getDownloadURL response in devtools for the backup id to see the actual shape and fields.
  2. If the object is missing in storage (expired lifecycle rule), regenerate the backup before download and align storage retention with DB retention.
  3. Verify the storage credentials have permission to generate presigned GET URLs (s3:GetObject).
  4. 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

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


AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15). Data as JSON: /api/errors/7803d932a5ce6bf1. Report an issue: GitHub.