toeverything/AFFiNE · error · BlobInvalid
blob_invalid
blob_invalid
Error message
Blob size mismatch
What it means
Thrown by BlobResolver.createBlobUpload when a blob record already exists for (workspaceId, key) but the declared size differs from record.size — the session's declared size is immutable, so re-initializing the same key with a different size is rejected as BlobInvalid('Blob size mismatch').
Solutions
- Reuse the exact size (and key) from the original createBlobUpload call when retrying
- If content/size genuinely changed, upload under a new unique blob key
- Delete the stale blob record/completed blob before re-initializing with different metadata
- Derive key+size once per file client-side and reuse them across the whole retry sequence
Example fix
// before const init = await createBlobUpload(wsId, key, file.size); // file still growing → mismatch on retry // after const size = await settledFileSize(file); // stat once after write completes const init = await createBlobUpload(wsId, key, size); // always same (key, size) pair
Defensive patterns
Strategy: validation
Validate before calling
// freeze (key, size) once per file and reuse across retries
const uploadDescriptor = {
key: await deriveBlobKey(file),
size: await finalSize(file), // stat only after the file is fully written
};
// every retry path uses uploadDescriptor.size — never re-stat mid-write
const init = await createBlobUpload(wsId, uploadDescriptor.key, uploadDescriptor.size, mime); Type guard
function isBlobSizeMismatch(e: unknown): e is { code: 'blob_invalid'; message: string } {
return typeof e === 'object' && e !== null && (e as any).code === 'blob_invalid' && /size mismatch/i.test((e as any).message ?? '');
} Try / catch
try {
return await createBlobUpload(wsId, key, size, mime);
} catch (e) {
if (isBlobSizeMismatch(e)) {
const freshKey = newBlobKey(); // content changed — restart under a new key
return createBlobUpload(wsId, freshKey, size, mime);
}
throw e;
} Prevention
- Compute key and size once per immutable file version and reuse them on every retry
- Only stat files after writes complete (no uploading files still being written)
- Don't share one blob key between concurrent uploads of different content
- In tests, derive keys from fixtures so sizes stay deterministic
When it happens
Trigger: Calling createBlobUpload twice for the same key with different size arguments: retries after the file changed underneath, two clients racing on one key, or a re-stat of a still-being-written file producing a new size.
Common situations: Retry logic that re-inits with freshly stat'ed metadata; concurrent uploads of same-named files; tests reusing fixed keys with variable-size fixtures.
Related errors
AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18).
Data as JSON: /api/errors/166839abd16a56d8.
Report an issue: GitHub.
Appendix: source
Thrown at packages/backend/server/src/core/workspaces/resolvers/blob.ts:258
mime: mime || 'application/octet-stream',
kind: 'blob',
});
this.assertStorageReservation(reservation);
if (reservation.alreadyUploaded) {
return {
method: BlobUploadMethod.GRAPHQL,
blobKey: key,
alreadyUploaded: true,
};
}
if (!reservation.reservationId)
throw new BlobInvalid('Missing blob reservation');
let record = await this.models.blob.get(workspaceId, key);
mime = mime || 'application/octet-stream';
if (record) {
if (record.size !== size) {
throw new BlobInvalid('Blob size mismatch');
}
if (record.status === 'completed') {
const existingMetadata = await this.storage.head(workspaceId, key);
if (!existingMetadata) {
// record exists but object is missing, treat as a new upload
record = null;
} else if (existingMetadata.contentLength !== size) {
throw new BlobInvalid('Blob size mismatch');
} else {
return {
method: BlobUploadMethod.GRAPHQL,
blobKey: key,
alreadyUploaded: true,
};
}
} else {
mime = record.mime;View on GitHub (pinned to 2af30773ae)