toeverything/AFFiNE · error · BlobNotFound
blob_not_found
blob_not_found
Error message
Blob ${blobId} not found in Space ${spaceId}. What it means
Thrown by completeBlobUpload when this.models.blob.get(workspaceId, key) returns no record. It means the upload was never initialized (no createBlobUpload / setBlob happened) for this workspace+key, or the record was garbage-collected before completion was called. BlobNotFound is a resource_not_found category error carrying spaceId and blobId in extensions.
Source
Thrown at packages/backend/server/src/core/workspaces/resolvers/blob.ts:356
@CurrentUser() user: CurrentUser,
@Args('workspaceId') workspaceId: string,
@Args('key') key: string,
@Args('uploadId', { nullable: true }) uploadId?: string,
@Args({
name: 'parts',
type: () => [BlobUploadPartInput],
nullable: true,
})
parts?: BlobUploadPartInput[]
): Promise<string> {
await this.ac
.user(user.id)
.workspace(workspaceId)
.assert('Workspace.Blobs.Write');
const record = await this.models.blob.get(workspaceId, key);
if (!record) {
throw new BlobNotFound({ spaceId: workspaceId, blobId: key });
}
if (record.status === 'completed') {
return key;
}
const hasMultipartInput =
uploadId !== undefined || (parts?.length ?? 0) > 0;
const hasMultipartRecord = !!record.uploadId;
if (hasMultipartRecord) {
if (!uploadId || !parts || parts.length === 0) {
throw new BlobInvalid(
'Multipart upload requires both uploadId and parts'
);
}
if (uploadId !== record.uploadId) {
throw new BlobInvalid('Upload id mismatch');
}
View on GitHub (pinned to 26c515e050)
Solutions
- Ensure createBlobUpload (or setBlob) is called and succeeds before completeBlobUpload for the same workspaceId+key.
- On receiving this error, restart the upload flow: generate/init a fresh blob record then re-upload.
- Confirm the workspaceId and key exactly match the values returned from createBlobUpload.
- If records are being reaped quickly, raise the pending-blob retention window for the deployment.
Example fix
// before
await gql.completeBlobUpload({ workspaceId, key });
// after - re-initialize when the record is missing
try {
await gql.completeBlobUpload({ workspaceId, key });
} catch (e) {
if (e.extensions?.code === 'blob_not_found') {
const init = await gql.createBlobUpload({ workspaceId, key, size, mime });
await uploadVia(init);
await gql.completeBlobUpload({ workspaceId, key, ... });
}
} Defensive patterns
Strategy: validation
Validate before calling
// Track in-flight blob keys so completion is always paired with an init
const inflight = new Map<string, boolean>();
inflight.set(key, true); // set right after createBlobUpload succeeds
if (!inflight.has(key)) {
throw new Error('No pending upload for key - call createBlobUpload first');
} Type guard
function isBlobNotFound(e: unknown): boolean {
return (
typeof e === 'object' &&
e !== null &&
(e as any).extensions?.code === 'blob_not_found'
);
} Try / catch
try {
await gql.completeBlobUpload({ workspaceId, key });
} catch (e) {
if (isBlobNotFound(e)) {
const init = await gql.createBlobUpload({ workspaceId, key, size, mime });
await uploadVia(init);
await gql.completeBlobUpload({ workspaceId, key });
return;
}
throw e;
} Prevention
- Always pair completeBlobUpload with a successful createBlobUpload for the same key.
- Keep the key/workspaceId returned by init until completion succeeds.
- Do not cache keys across long-lived sessions; re-init after resume.
When it happens
Trigger: Calling completeBlobUpload without a prior successful createBlobUpload for the same key; calling it after the blob record expired or was deleted; passing a key from a different workspace; replaying an old key after the workspace was reset.
Common situations: Client retries completion after a long pause and the pending record was reaped; race where setBlob/upsert never committed; frontend generated a key locally but never called createBlobUpload; wrong workspaceId in a multi-workspace client.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/646dff844f0c2be8.
Report an issue: GitHub.