{"record":{"id":"6ab3b0dfaf620823","repo":"toeverything/AFFiNE","slug":"comment-attachment-not-found","errorCode":"comment_attachment_not_found","errorMessage":"Comment attachment not found.","messagePattern":"Comment attachment not found\\.","errorType":"exception","errorClass":"CommentAttachmentNotFound","httpStatus":404,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/controller.ts","lineNumber":393,"sourceCode":"  @CallMetric('controllers', 'workspace_get_comment_attachment')\n  async commentAttachment(\n    @CurrentUser() user: CurrentUser,\n    @Param('id') workspaceId: string,\n    @Param('docId') docId: string,\n    @Param('key') key: string,\n    @Res() res: Response\n  ) {\n    await this.ac.user(user.id).doc(workspaceId, docId).assert('Doc.Read');\n\n    const { body, metadata, redirectUrl } =\n      await this.commentAttachmentStorage.get(workspaceId, docId, key, true);\n\n    if (redirectUrl) {\n      return res.redirect(redirectUrl);\n    }\n\n    if (!body) {\n      throw new CommentAttachmentNotFound();\n    }\n\n    // metadata should always exists if body is not null\n    if (metadata) {\n      res.setHeader('content-type', metadata.contentType);\n      res.setHeader('last-modified', metadata.lastModified.toUTCString());\n      res.setHeader('content-length', metadata.contentLength);\n    } else {\n      this.logger.warn(\n        `Comment attachment ${workspaceId}/${docId}/${key} has no metadata`\n      );\n    }\n    applyAttachHeaders(res, {\n      contentType: metadata?.contentType,\n      filename: key,\n    });\n\n    res.setHeader('cache-control', 'private, max-age=2592000, immutable');","sourceCodeStart":375,"sourceCodeEnd":411,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/workspaces/controller.ts#L375-L411","documentation":"Thrown by the comment-attachment route (GET /:id/docs/:docId/comment-attachments/:key) when commentAttachmentStorage.get(workspaceId, docId, key, true) returns neither a body nor a redirectUrl, after the Doc.Read assert passed. Plain storage-layer miss for comment media.","triggerScenarios":"Fetching a comment attachment whose key was never uploaded, was removed, or whose object is gone from the storage backend with no signed-url fallback available.","commonSituations":"Comments referencing deleted attachments; storage migration losing objects; wrong key passed by the client; uploads that failed before the object write but after the comment was created.","solutions":["Verify the key against the comment's stored attachment data before fetching","Re-upload the attachment and update the comment if the media matters","Audit comment-attachment storage config if objects vanish systematically","Render a placeholder in the UI on 404 rather than erroring the whole comment thread"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isCommentAttachmentNotFound(e: unknown): e is { code: 'comment_attachment_not_found' } {\n  return typeof e === 'object' && e !== null && (e as any).code === 'comment_attachment_not_found';\n}","tryCatchPattern":"try {\n  return await fetchCommentAttachment(wsId, docId, key);\n} catch (e) {\n  if (isCommentAttachmentNotFound(e)) {\n    return renderAttachmentPlaceholder(); // keep the comment thread usable\n  }\n  throw e;\n}","preventionTips":["Only reference attachment keys the upload flow confirmed","On attachment deletion, update the owning comment's payload","Verify comment-attachment storage config after migrations","Design comment UI to degrade gracefully per-attachment"],"tags":["not-found","comment","attachment","storage"],"backgroundTag":"resource-not-found","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}