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

  1. Reuse the exact size (and key) from the original createBlobUpload call when retrying
  2. If content/size genuinely changed, upload under a new unique blob key
  3. Delete the stale blob record/completed blob before re-initializing with different metadata
  4. 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

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)