{"record":{"id":"646dff844f0c2be8","repo":"toeverything/AFFiNE","slug":"blob-not-found-646dff","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/core/workspaces/resolvers/blob.ts","lineNumber":356,"sourceCode":"    @CurrentUser() user: CurrentUser,\n    @Args('workspaceId') workspaceId: string,\n    @Args('key') key: string,\n    @Args('uploadId', { nullable: true }) uploadId?: string,\n    @Args({\n      name: 'parts',\n      type: () => [BlobUploadPartInput],\n      nullable: true,\n    })\n    parts?: BlobUploadPartInput[]\n  ): Promise<string> {\n    await this.ac\n      .user(user.id)\n      .workspace(workspaceId)\n      .assert('Workspace.Blobs.Write');\n\n    const record = await this.models.blob.get(workspaceId, key);\n    if (!record) {\n      throw new BlobNotFound({ spaceId: workspaceId, blobId: key });\n    }\n    if (record.status === 'completed') {\n      return key;\n    }\n\n    const hasMultipartInput =\n      uploadId !== undefined || (parts?.length ?? 0) > 0;\n    const hasMultipartRecord = !!record.uploadId;\n    if (hasMultipartRecord) {\n      if (!uploadId || !parts || parts.length === 0) {\n        throw new BlobInvalid(\n          'Multipart upload requires both uploadId and parts'\n        );\n      }\n      if (uploadId !== record.uploadId) {\n        throw new BlobInvalid('Upload id mismatch');\n      }\n","sourceCodeStart":338,"sourceCodeEnd":374,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/blob.ts#L338-L374","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait gql.completeBlobUpload({ workspaceId, key });\n\n// after - re-initialize when the record is missing\ntry {\n  await gql.completeBlobUpload({ workspaceId, key });\n} catch (e) {\n  if (e.extensions?.code === 'blob_not_found') {\n    const init = await gql.createBlobUpload({ workspaceId, key, size, mime });\n    await uploadVia(init);\n    await gql.completeBlobUpload({ workspaceId, key, ... });\n  }\n}","handlingStrategy":"validation","validationCode":"// Track in-flight blob keys so completion is always paired with an init\nconst inflight = new Map<string, boolean>();\ninflight.set(key, true);            // set right after createBlobUpload succeeds\nif (!inflight.has(key)) {\n  throw new Error('No pending upload for key - call createBlobUpload first');\n}","typeGuard":"function isBlobNotFound(e: unknown): boolean {\n  return (\n    typeof e === 'object' &&\n    e !== null &&\n    (e as any).extensions?.code === 'blob_not_found'\n  );\n}","tryCatchPattern":"try {\n  await gql.completeBlobUpload({ workspaceId, key });\n} catch (e) {\n  if (isBlobNotFound(e)) {\n    const init = await gql.createBlobUpload({ workspaceId, key, size, mime });\n    await uploadVia(init);\n    await gql.completeBlobUpload({ workspaceId, key });\n    return;\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["blob-storage","graphql","upload-lifecycle","not-found"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}