{"record":{"id":"c034de86c70da503","repo":"halo-dev/halo","slug":"attachment-has-no-permalink","errorCode":null,"errorMessage":"Attachment has no permalink","messagePattern":"Attachment has no permalink","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ui/console-src/modules/contents/attachments/components/AttachmentListItem.vue","lineNumber":102,"sourceCode":"  computed((): OperationItem<Attachment>[] => [\n    {\n      priority: 10,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.detail\"),\n      permissions: [],\n      action: () => {\n        emit(\"open-detail\", attachment.value);\n      },\n    },\n    {\n      priority: 20,\n      component: markRaw(VDropdownItem),\n      label: t(\"core.common.buttons.download\"),\n      action: () => {\n        const { permalink } = attachment.value.status || {};\n\n        if (!permalink) {\n          throw new Error(\"Attachment has no permalink\");\n        }\n\n        const a = document.createElement(\"a\");\n        a.href = permalink;\n        a.download = attachment.value.spec.displayName || \"unknown\";\n        document.body.appendChild(a);\n        a.click();\n        document.body.removeChild(a);\n      },\n    },\n    {\n      priority: 30,\n      component: markRaw(VDropdownDivider),\n    },\n    {\n      priority: 40,\n      component: markRaw(VDropdownItem),\n      props: {","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/halo-dev/halo/blob/d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8/ui/console-src/modules/contents/attachments/components/AttachmentListItem.vue#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Wait for the attachment status to reconcile (refetch the attachment list) and retry download once status.permalink is populated.","Verify the storage policy plugin sets status.permalink — for local policy ensure the external link/permalink generation is enabled.","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.","Guard the Download menu item: hide or disable it when !attachment.status?.permalink so the throw never fires."],"exampleFix":"// before\nconst { permalink } = attachment.value.status || {};\nif (!permalink) {\n  throw new Error(\"Attachment has no permalink\");\n}\n// after — fall back to an authenticated fetch-by-name endpoint, or disable the menu item\nconst { permalink } = attachment.value.status || {};\nif (!permalink) {\n  Toast.warning(t(\"core.attachment.no_permalink\"));\n  return;\n}","handlingStrategy":"type-guard","validationCode":"// Check permalink presence before offering/enabling download\nfunction getDownloadUrl(attachment): string | null {\n  const p = attachment?.status?.permalink;\n  return typeof p === \"string\" && p.length > 0 ? p : null;\n}\n// const url = getDownloadUrl(attachment.value); if (!url) { Toast.warning(...); return; }","typeGuard":"function attachmentHasPermalink(a: { status?: { permalink?: unknown } }): a is { status: { permalink: string } } {\n  return typeof a?.status?.permalink === \"string\" && (a.status.permalink as string).length > 0;\n}","tryCatchPattern":"// The download action is sync; wrap to avoid an unhandled throw in the dropdown handler\naction: () => {\n  try {\n    const { permalink } = attachment.value.status || {};\n    if (!permalink) throw new Error(\"Attachment has no permalink\");\n    triggerDownload(permalink, attachment.value.spec.displayName || \"unknown\");\n  } catch (e) {\n    Toast.error(e instanceof Error ? e.message : \"Download failed\");\n  }\n}","preventionTips":["Disable or hide the Download menu item when !attachment.status?.permalink.","Ensure storage policy plugins set status.permalink during reconciliation.","For private/local storage without a permalink, implement a backend stream endpoint and fall back to it."],"tags":["attachments","storage-policy","download","vue","permalink","validation"],"backgroundTag":null,"analyzedSha":"d2f5165f9c8f055ffcb3fa9c3f4032821a7b68c8","analyzedAt":"2026-08-14T00:18:38.915Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}