Wei-Shaw/sub2api · warning

profile.avatar.compressFailed

Error message

profile.avatar.compressFailed

What it means

In frontend/src/components/user/profile/ProfileAvatarCard.vue:168, compressAvatarFile() calls canvas.getContext('2d') and throws the localized 'profile.avatar.compressFailed' if it returns null. getContext('2d') returns null when the canvas element cannot provide a 2D context — most commonly because a different context type was already obtained on the same canvas, the browser has exhausted canvas memory/GPU contexts, or the document environment doesn't support 2D rendering.

Source

Thrown at frontend/src/components/user/profile/ProfileAvatarCard.vue:168

function canvasToBlob(canvas: HTMLCanvasElement, type: string, quality: number): Promise<Blob> {
  return new Promise((resolve, reject) => {
    canvas.toBlob((blob) => {
      if (!blob) {
        reject(new Error(t('profile.avatar.compressFailed')))
        return
      }
      resolve(blob)
    }, type, quality)
  })
}

async function compressAvatarFile(file: File): Promise<File> {
  const sourceDataURL = await readFileAsDataURL(file)
  const image = await loadImage(sourceDataURL)
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')
  if (!ctx) {
    throw new Error(t('profile.avatar.compressFailed'))
  }

  for (const scale of avatarScaleSteps) {
    const width = Math.max(1, Math.round(image.naturalWidth * scale))
    const height = Math.max(1, Math.round(image.naturalHeight * scale))
    canvas.width = width
    canvas.height = height
    ctx.clearRect(0, 0, width, height)
    ctx.drawImage(image, 0, 0, width, height)

    for (const quality of avatarQualitySteps) {
      const blob = await canvasToBlob(canvas, 'image/webp', quality)
      if (blob.size <= targetAvatarUploadBytes) {
        const fileName = file.name.replace(/\.[^.]+$/, '') || 'avatar'
        return new File([blob], `${fileName}.webp`, { type: 'image/webp' })
      }
    }
  }

View on GitHub (pinned to 073e92d171)

Solutions

  1. Create a fresh canvas per compression call (the code does this — audit for any reuse with webgl) and release old ones by zeroing width/height.
  2. In tests, install the 'canvas' npm package so jsdom returns a real 2D context.
  3. Catch the error and offer the original file (or reject large files) instead of dead-ending the upload UI.
  4. If it fails in production, prompt a page reload — GPU/context exhaustion usually recovers after refresh.

Example fix

// before
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
  throw new Error(t('profile.avatar.compressFailed'))
}

// after
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d', { willReadFrequently: true })
if (!ctx) {
  if (file.size <= hardAvatarLimitBytes) return file  // last resort: accept original
  throw new Error(t('profile.avatar.compressFailed'))
}
Defensive patterns

Strategy: fallback

Validate before calling

const probeCtx = document.createElement('canvas').getContext('2d');
export const canvas2dAvailable = !!probeCtx;
if (!canvas2dAvailable) disableAvatarCompression();

Type guard

function has2dContext(ctx: CanvasRenderingContext2D | null): ctx is CanvasRenderingContext2D {
  return ctx !== null;
}

Try / catch

try { file = await compressAvatarFile(file); }
catch (e) {
  if (e.message === t('profile.avatar.compressFailed') && file.size <= hardLimit) return file; // degrade to original
  throw e;
}

Prevention

When it happens

Trigger: Calling getContext('2d') after getContext('webgl') on the same canvas; too many live canvases exhausting the browser's context limit (common in long sessions or pages creating many canvases); Chrome's GPU process crash disabling accelerated 2D contexts; running in a DOM-less test environment with an incompletely polyfilled canvas.

Common situations: Memory leaks in SPAs accumulating canvases (avatar re-uploads in a long-lived profile page); jsdom tests without the node-canvas polyfill; browsers after a GPU driver reset where context creation fails until reload.

Related errors


AI-assisted analysis of Wei-Shaw/sub2api@073e92d171 (2026-08-15). Data as JSON: /api/errors/466919a0cba92b81. Report an issue: GitHub.