yikart/AiToEarn · error · Error

Failed to create blob

Error message

Failed to create blob

What it means

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.

Source

Thrown at project/aitoearn-web/src/components/PublishDialog/compoents/PubParmasTextarea/VideoCoverSeting.tsx:188

      if (cropper.current) {
        cropper.current.setAspectRatio(ratio ?? Number.NaN)
      }
    }, [])

    /** 确认选择封面 */
    const handleConfirm = useCallback(async () => {
      if (!cropper.current || !imgFile)
        return

      setUploadLoading(true)
      try {
        const canvas = cropper.current.getCroppedCanvas()
        const blob = await new Promise<Blob | null>((resolve) => {
          canvas.toBlob(resolve, 'image/jpeg', 0.92)
        })

        if (!blob) {
          throw new Error('Failed to create blob')
        }

        const cover = await formatImg({
          blob,
          path: `${saveImgId}.jpg`,
        })

        // 上传封面到 OSS
        const uploadCoverRes = await uploadToOss(cover.file)
        cover.ossUrl = getOssUrl(uploadCoverRes)

        onChoosed(cover)
        handleClose()
      }
      catch (error) {
        console.error('上传封面失败:', error)
        toast.error(t('videoCover.uploadFailed'))
      }

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Load the cover image through the same-origin OSS proxy (getOssProxyPath) to prevent canvas tainting
  2. Guard that getCroppedCanvas() returns a canvas with width>0 && height>0 before toBlob
  3. Wait for image onload / cropper ready before enabling the confirm button
  4. Retry toBlob once; if still null, surface a specific crop error message

Example fix

// before
const canvas = cropper.current.getCroppedCanvas()
const blob = await new Promise<Blob | null>((resolve) => {
  canvas.toBlob(resolve, 'image/jpeg', 0.92)
})
// after
const canvas = cropper.current.getCroppedCanvas()
if (!canvas || canvas.width === 0 || canvas.height === 0)
  throw new Error('Cropped canvas is empty')
const blob = await new Promise<Blob | null>((resolve, reject) => {
  canvas.toBlob((b) => b ? resolve(b) : reject(new Error('toBlob returned null')), 'image/jpeg', 0.92)
})
Defensive patterns

Strategy: validation

Validate before calling

const canvas = cropper.current.getCroppedCanvas()
if (!canvas || canvas.width === 0 || canvas.height === 0)
  throw new Error('empty cropped canvas')

Type guard

function isBlob(v: Blob | null | undefined): v is Blob {
  return v instanceof Blob && v.size > 0
}

Try / catch

try {
  const blob = await new Promise<Blob | null>(r => canvas.toBlob(r, 'image/jpeg', 0.92))
  if (!isBlob(blob)) throw new Error('Failed to create blob')
} catch (e) {
  toast.error(t('videoCover.uploadFailed'))
}

Prevention

When it happens

Trigger: getCroppedCanvas() returned a canvas with zero width/height, or the canvas is tainted by cross-origin image data, causing canvas.toBlob to yield null.

Common situations: 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.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/de3a5d63c5227690. Report an issue: GitHub.