halo-dev/halo · error · Error

Attachment has no permalink

Error message

Attachment has no permalink

What it means

Thrown in AttachmentListItem.vue's download dropdown action when the attachment has no permalink in its status. Halo attachments expose a downloadable URL via status.permalink (populated by the storage policy / attachment reconciler). When that field is absent — attachment not yet reconciled, storage policy producing no permalink, or local storage not generating a public link — the download action throws rather than triggering a broken anchor click.

Source

Thrown at ui/console-src/modules/contents/attachments/components/AttachmentListItem.vue:102

  computed((): OperationItem<Attachment>[] => [
    {
      priority: 10,
      component: markRaw(VDropdownItem),
      label: t("core.common.buttons.detail"),
      permissions: [],
      action: () => {
        emit("open-detail", attachment.value);
      },
    },
    {
      priority: 20,
      component: markRaw(VDropdownItem),
      label: t("core.common.buttons.download"),
      action: () => {
        const { permalink } = attachment.value.status || {};

        if (!permalink) {
          throw new Error("Attachment has no permalink");
        }

        const a = document.createElement("a");
        a.href = permalink;
        a.download = attachment.value.spec.displayName || "unknown";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      },
    },
    {
      priority: 30,
      component: markRaw(VDropdownDivider),
    },
    {
      priority: 40,
      component: markRaw(VDropdownItem),
      props: {

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Wait for the attachment status to reconcile (refetch the attachment list) and retry download once status.permalink is populated.
  2. Verify the storage policy plugin sets status.permalink — for local policy ensure the external link/permalink generation is enabled.
  3. If permalink is intentionally absent (private storage), route the download through a backend endpoint that streams the file by attachment name, rather than relying on status.permalink.
  4. Guard the Download menu item: hide or disable it when !attachment.status?.permalink so the throw never fires.

Example fix

// before
const { permalink } = attachment.value.status || {};
if (!permalink) {
  throw new Error("Attachment has no permalink");
}
// after — fall back to an authenticated fetch-by-name endpoint, or disable the menu item
const { permalink } = attachment.value.status || {};
if (!permalink) {
  Toast.warning(t("core.attachment.no_permalink"));
  return;
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Check permalink presence before offering/enabling download
function getDownloadUrl(attachment): string | null {
  const p = attachment?.status?.permalink;
  return typeof p === "string" && p.length > 0 ? p : null;
}
// const url = getDownloadUrl(attachment.value); if (!url) { Toast.warning(...); return; }

Type guard

function attachmentHasPermalink(a: { status?: { permalink?: unknown } }): a is { status: { permalink: string } } {
  return typeof a?.status?.permalink === "string" && (a.status.permalink as string).length > 0;
}

Try / catch

// The download action is sync; wrap to avoid an unhandled throw in the dropdown handler
action: () => {
  try {
    const { permalink } = attachment.value.status || {};
    if (!permalink) throw new Error("Attachment has no permalink");
    triggerDownload(permalink, attachment.value.spec.displayName || "unknown");
  } catch (e) {
    Toast.error(e instanceof Error ? e.message : "Download failed");
  }
}

Prevention

When it happens

Trigger: User clicks the Download dropdown item on an attachment whose status.permalink is empty/undefined. Happens when the attachment upload finished but the status subresource hasn't been reconciled yet, the storage policy (e.g. local without external link, or a misconfigured S3/OSS plugin) does not populate permalink, or the attachment was created out-of-band without a status.

Common situations: Custom storage policy plugin that forgets to set status.permalink; race condition clicking download immediately after upload before reconciliation; attachment migrated/imported without status reconciliation; local storage policy where permalink is intentionally null and download must go through an API endpoint instead.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/c034de86c70da503. Report an issue: GitHub.