toeverything/AFFiNE · error · BlobNotFound
blob_not_found
blob_not_found
Error message
Blob ${blobId} not found in Space ${spaceId}. What it means
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.
Source
Thrown at packages/backend/server/src/plugins/copilot/controller.ts:357
@Res() res: Response,
@Param('userId') userId: string,
@Param('workspaceId') workspaceId: string,
@Param('key') key: string
) {
const { body, metadata, redirectUrl } = await this.storage.get(
userId,
workspaceId,
key,
true
);
if (redirectUrl) {
// redirect to signed url
return res.redirect(redirectUrl);
}
if (!body) {
throw new BlobNotFound({
spaceId: workspaceId,
blobId: key,
});
}
// metadata should always exists if body is not null
if (metadata) {
res.setHeader('content-type', metadata.contentType);
res.setHeader('last-modified', metadata.lastModified.toUTCString());
res.setHeader('content-length', metadata.contentLength);
} else {
this.logger.warn(`Blob ${workspaceId}/${key} has no metadata`);
}
applyAttachHeaders(res, {
contentType: metadata?.contentType,
filename: key,
});
View on GitHub (pinned to b4c8548c09)
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.
Example fix
null
Defensive patterns
Strategy: try-catch
Validate before calling
// Before rendering, HEAD-check the blob URL exists
const ok = await fetch(blobUrl, { method: 'HEAD' }).then(r => r.ok).catch(() => false);
if (!ok) renderPlaceholder(); Type guard
const isBlobNotFound = (e: unknown): boolean => !!e && typeof e === 'object' && (e as any).code === 'blob_not_found';
Try / catch
try {
const blob = await copilot.getBlob(workspaceId, key);
} catch (e) {
if (isBlobNotFound(e)) {
renderPlaceholder({ blobId: e.extensions?.blobId, spaceId: e.extensions?.spaceId });
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18).
Data as JSON: /api/errors/4aa37ff9c75331de.
Report an issue: GitHub.