{"record":{"id":"d1d2553684dede8f","repo":"Wei-Shaw/sub2api","slug":"profile-avatar-compresstoolarge","errorCode":null,"errorMessage":"profile.avatar.compressTooLarge","messagePattern":"profile\\.avatar\\.compressTooLarge","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"frontend/src/components/user/profile/ProfileAvatarCard.vue","lineNumber":188,"sourceCode":"\n  for (const scale of avatarScaleSteps) {\n    const width = Math.max(1, Math.round(image.naturalWidth * scale))\n    const height = Math.max(1, Math.round(image.naturalHeight * scale))\n    canvas.width = width\n    canvas.height = height\n    ctx.clearRect(0, 0, width, height)\n    ctx.drawImage(image, 0, 0, width, height)\n\n    for (const quality of avatarQualitySteps) {\n      const blob = await canvasToBlob(canvas, 'image/webp', quality)\n      if (blob.size <= targetAvatarUploadBytes) {\n        const fileName = file.name.replace(/\\.[^.]+$/, '') || 'avatar'\n        return new File([blob], `${fileName}.webp`, { type: 'image/webp' })\n      }\n    }\n  }\n\n  throw new Error(t('profile.avatar.compressTooLarge'))\n}\n\nasync function prepareAvatarUpload(file: File): Promise<File> {\n  if (!file.type.startsWith('image/')) {\n    throw new Error(t('profile.avatar.invalidType'))\n  }\n  if (file.type === 'image/gif') {\n    if (file.size > targetAvatarUploadBytes) {\n      throw new Error(t('profile.avatar.gifTooLarge'))\n    }\n    return file\n  }\n  if (file.size <= targetAvatarUploadBytes) {\n    return file\n  }\n  return compressAvatarFile(file)\n}\n","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/Wei-Shaw/sub2api/blob/073e92d17178a1ccdb0a27017f572f10c9c7ab62/frontend/src/components/user/profile/ProfileAvatarCard.vue#L170-L206","documentation":"In frontend/src/components/user/profile/ProfileAvatarCard.vue:188, compressAvatarFile() iterates a ladder of downscale steps (avatarScaleSteps) and webp quality steps (avatarQualitySteps); if no scale/quality combination produces a blob under targetAvatarUploadBytes, it throws the localized 'profile.avatar.compressTooLarge'. The image cannot be shrunk to the upload budget — typically a huge source image or a pathological image that webp encodes poorly.","triggerScenarios":"Uploading a very high-resolution photo (e.g., 8000×6000) where even the smallest scale step keeps the encode above targetAvatarUploadBytes; images with noisy/photographic content that resist webp compression; browsers whose canvas.toBlob for image/webp falls back to png (older Safari), yielding larger blobs than expected.","commonSituations":"Panorama or RAW-export uploads; screenshots with gradients; Safari < 14 lacking solid webp encoding so every quality step returns similar sizes; targetAvatarUploadBytes set very low by configuration.","solutions":["Extend the scale ladder with smaller final scales (e.g., down to 128px) so the last step nearly always fits.","Detect toBlob('image/webp') support (Safari fallback) and use 'image/jpeg' quality ladder as fallback output type.","Reject early with a clear message when naturalWidth*smallestScale still can't plausibly fit, before doing the expensive loop.","Raise targetAvatarUploadBytes if the product allows larger avatars."],"exampleFix":"// before\nfor (const scale of avatarScaleSteps) {\n  // ... for (const quality of avatarQualitySteps) { ... }\n}\nthrow new Error(t('profile.avatar.compressTooLarge'))\n\n// after\nconst outType = canvasToBlobSupportsWebp() ? 'image/webp' : 'image/jpeg'\nfor (const scale of avatarScaleSteps) {\n  for (const quality of avatarQualitySteps) {\n    const blob = await canvasToBlob(canvas, outType, quality)\n    if (blob.size <= targetAvatarUploadBytes) return new File([blob], `${fileName}.${outType.split('/')[1]}`, { type: outType })\n  }\n}\nthrow new Error(t('profile.avatar.compressTooLarge'))","handlingStrategy":"validation","validationCode":"// Reject implausibly huge images before the expensive compression ladder:\nconst minPixels = 128 * 128;\nif (image.naturalWidth * image.naturalHeight < minPixels && file.size > targetAvatarUploadBytes * 2) {\n  throw new Error(t('profile.avatar.compressTooLarge'));\n}","typeGuard":null,"tryCatchPattern":"try { avatar = await compressAvatarFile(file); }\ncatch (e) {\n  if (e.message === t('profile.avatar.compressTooLarge')) { showSizeHint(); return; }\n  throw e;\n}","preventionTips":["Extend the scale ladder to very small final dimensions so the last rung nearly always fits","Detect webp toBlob support (Safari) and fall back to jpeg quality steps","Show the target byte budget in the UI before upload"],"tags":["image-compression","webp","avatar","upload-limits","frontend"],"backgroundTag":null,"analyzedSha":"073e92d17178a1ccdb0a27017f572f10c9c7ab62","analyzedAt":"2026-08-15T14:33:00.750Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}