{"record":{"id":"de3a5d63c5227690","repo":"yikart/AiToEarn","slug":"failed-to-create-blob","errorCode":null,"errorMessage":"Failed to create blob","messagePattern":"Failed to create blob","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"project/aitoearn-web/src/components/PublishDialog/compoents/PubParmasTextarea/VideoCoverSeting.tsx","lineNumber":188,"sourceCode":"      if (cropper.current) {\n        cropper.current.setAspectRatio(ratio ?? Number.NaN)\n      }\n    }, [])\n\n    /** 确认选择封面 */\n    const handleConfirm = useCallback(async () => {\n      if (!cropper.current || !imgFile)\n        return\n\n      setUploadLoading(true)\n      try {\n        const canvas = cropper.current.getCroppedCanvas()\n        const blob = await new Promise<Blob | null>((resolve) => {\n          canvas.toBlob(resolve, 'image/jpeg', 0.92)\n        })\n\n        if (!blob) {\n          throw new Error('Failed to create blob')\n        }\n\n        const cover = await formatImg({\n          blob,\n          path: `${saveImgId}.jpg`,\n        })\n\n        // 上传封面到 OSS\n        const uploadCoverRes = await uploadToOss(cover.file)\n        cover.ossUrl = getOssUrl(uploadCoverRes)\n\n        onChoosed(cover)\n        handleClose()\n      }\n      catch (error) {\n        console.error('上传封面失败:', error)\n        toast.error(t('videoCover.uploadFailed'))\n      }","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-web/src/components/PublishDialog/compoents/PubParmasTextarea/VideoCoverSeting.tsx#L170-L206","documentation":"VideoCoverSeting's handleConfirm throws this Error when the cropped cropper canvas's toBlob('image/jpeg', 0.92) resolves null, meaning the browser could not encode the cropped canvas to a JPEG blob. The catch block shows a generic uploadFailed toast.","triggerScenarios":"getCroppedCanvas() returned a canvas with zero width/height, or the canvas is tainted by cross-origin image data, causing canvas.toBlob to yield null.","commonSituations":"User confirms crop before the image fully loads; cropper container has 0 size; cover image loaded from a cross-origin OSS URL without the same-origin proxy (getOssProxyPath), tainting the canvas.","solutions":["Load the cover image through the same-origin OSS proxy (getOssProxyPath) to prevent canvas tainting","Guard that getCroppedCanvas() returns a canvas with width>0 && height>0 before toBlob","Wait for image onload / cropper ready before enabling the confirm button","Retry toBlob once; if still null, surface a specific crop error message"],"exampleFix":"// before\nconst canvas = cropper.current.getCroppedCanvas()\nconst blob = await new Promise<Blob | null>((resolve) => {\n  canvas.toBlob(resolve, 'image/jpeg', 0.92)\n})\n// after\nconst canvas = cropper.current.getCroppedCanvas()\nif (!canvas || canvas.width === 0 || canvas.height === 0)\n  throw new Error('Cropped canvas is empty')\nconst blob = await new Promise<Blob | null>((resolve, reject) => {\n  canvas.toBlob((b) => b ? resolve(b) : reject(new Error('toBlob returned null')), 'image/jpeg', 0.92)\n})","handlingStrategy":"validation","validationCode":"const canvas = cropper.current.getCroppedCanvas()\nif (!canvas || canvas.width === 0 || canvas.height === 0)\n  throw new Error('empty cropped canvas')","typeGuard":"function isBlob(v: Blob | null | undefined): v is Blob {\n  return v instanceof Blob && v.size > 0\n}","tryCatchPattern":"try {\n  const blob = await new Promise<Blob | null>(r => canvas.toBlob(r, 'image/jpeg', 0.92))\n  if (!isBlob(blob)) throw new Error('Failed to create blob')\n} catch (e) {\n  toast.error(t('videoCover.uploadFailed'))\n}","preventionTips":["Load the cover image through the same-origin OSS proxy so the canvas is not tainted","Only enable confirm after the image has loaded and the cropper is initialized","Check cropped canvas dimensions before toBlob","Retry once on null toBlob before failing"],"tags":["canvas","cropper","blob","cross-origin"],"backgroundTag":"canvas-tainted-or-empty","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}