{"record":{"id":"4aa37ff9c75331de","repo":"toeverything/AFFiNE","slug":"blob-not-found-4aa37f","errorCode":"blob_not_found","errorMessage":"Blob ${blobId} not found in Space ${spaceId}.","messagePattern":"Blob (.+?) not found in Space (.+?)\\.","errorType":"exception","errorClass":"BlobNotFound","httpStatus":404,"severity":"error","filePath":"packages/backend/server/src/plugins/copilot/controller.ts","lineNumber":357,"sourceCode":"    @Res() res: Response,\n    @Param('userId') userId: string,\n    @Param('workspaceId') workspaceId: string,\n    @Param('key') key: string\n  ) {\n    const { body, metadata, redirectUrl } = await this.storage.get(\n      userId,\n      workspaceId,\n      key,\n      true\n    );\n\n    if (redirectUrl) {\n      // redirect to signed url\n      return res.redirect(redirectUrl);\n    }\n\n    if (!body) {\n      throw new BlobNotFound({\n        spaceId: workspaceId,\n        blobId: key,\n      });\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(`Blob ${workspaceId}/${key} has no metadata`);\n    }\n    applyAttachHeaders(res, {\n      contentType: metadata?.contentType,\n      filename: key,\n    });\n","sourceCodeStart":339,"sourceCodeEnd":375,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/controller.ts#L339-L375","documentation":"Thrown by the copilot blob endpoint when the requested blob cannot be served: there is no signed redirect URL and the storage backend returned no body for the workspace+key. BlobNotFound carries the spaceId (workspaceId) and blobId (key), and indicates the object genuinely does not exist in storage — deleted, never uploaded, or referenced by a stale id.","triggerScenarios":"GET of a blob/COP file whose key was deleted (workspace cleanup, blob GC) while a message or doc still references it; client builds the URL from an old blob id after the provider response changed; upload to storage succeeded partially (body missing) or the storage bucket was wiped; wrong workspace id in the URL so the lookup targets another space's namespace.","commonSituations":"Chat history rendering old AI images whose underlying blobs were garbage-collected; S3-compatible storage with lifecycle rules deleting objects; migrations between storage backends that dropped objects; race where the blob URL is requested before the upload committed.","solutions":["Confirm the blob key exists in the backing storage for that exact workspace prefix (S3 ls / storage console).","If blobs were deleted, re-trigger generation/upload or purge the referencing message/doc so clients stop requesting dead keys.","Check storage lifecycle rules and exclude the workspace blob prefix from expiry.","For migration issues, re-run the blob migration/sync so all referenced keys exist in the new backend."],"exampleFix":"null","handlingStrategy":"try-catch","validationCode":"// Before rendering, HEAD-check the blob URL exists\nconst ok = await fetch(blobUrl, { method: 'HEAD' }).then(r => r.ok).catch(() => false);\nif (!ok) renderPlaceholder();","typeGuard":"const isBlobNotFound = (e: unknown): boolean =>\n  !!e && typeof e === 'object' && (e as any).code === 'blob_not_found';","tryCatchPattern":"try {\n  const blob = await copilot.getBlob(workspaceId, key);\n} catch (e) {\n  if (isBlobNotFound(e)) {\n    renderPlaceholder({ blobId: e.extensions?.blobId, spaceId: e.extensions?.spaceId });\n  } else throw e;\n}","preventionTips":["Exclude workspace blob storage prefixes from storage lifecycle/expiry rules.","Keep blob references and blobs transactionally coupled: delete references when blobs are GC'd.","Handle 404 on blob fetches gracefully (placeholder) since stale references in old content are expected.","Verify storage migrations copy every referenced key, and run a referential integrity check after."],"tags":["copilot","blob","storage","not-found","s3"],"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-14T05:17:10.506Z"}