{"record":{"id":"726f4811c24edd9e","repo":"santifer/career-ops","slug":"unsupported-profile-photo-format-photo-expect","errorCode":null,"errorMessage":"Unsupported profile photo format: ${photo} (expected PNG, JPEG, WebP, or GIF)","messagePattern":"Unsupported profile photo format: (.+?) \\(expected PNG, JPEG, WebP, or GIF\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"build-cv-html.mjs","lineNumber":142,"sourceCode":"      throw new Error('Unsupported profile photo data URL (expected base64 PNG, JPEG, WebP, or GIF)');\n    }\n    c.photo = photo;\n    return c;\n  }\n\n  if (/^https?:\\/\\//i.test(photo)) {\n    c.photo = photo;\n    return c;\n  }\n\n  if (/^[a-z][a-z0-9+.-]+:/i.test(photo)) {\n    throw new Error(`Unsupported profile photo URL scheme: ${photo.split(':', 1)[0]}`);\n  }\n\n  const photoPath = isAbsolute(photo) ? photo : resolve(__dirname, photo);\n  const mime = PHOTO_MIME_BY_EXT.get(extname(photoPath).toLowerCase());\n  if (!mime) {\n    throw new Error(`Unsupported profile photo format: ${photo} (expected PNG, JPEG, WebP, or GIF)`);\n  }\n\n  let bytes;\n  try {\n    bytes = await readFile(photoPath);\n  } catch (err) {\n    throw new Error(`Profile photo not found or unreadable: ${photo} (${err.code || err.message})`);\n  }\n  if (bytes.length === 0) {\n    throw new Error(`Profile photo is empty: ${photo}`);\n  }\n  c.photo = `data:${mime};base64,${bytes.toString('base64')}`;\n  return c;\n}\n\nfunction joinItems(items) {\n  if (Array.isArray(items)) return items.join(', ');\n  return typeof items === 'string' ? items : '';","sourceCodeStart":124,"sourceCodeEnd":160,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/build-cv-html.mjs#L124-L160","documentation":"Validation in `prepareCandidatePhoto`: for a local file path (not data URL, not http(s)), the extension must map to an allowed mime via `PHOTO_MIME_BY_EXT`. Only `.png`, `.jpg`/`.jpeg`, `.webp`, `.gif` are accepted; any other extension (or no extension) is rejected before any disk read, so unsupported formats never get embedded into the CV.","triggerScenarios":"Passing `photo: 'avatar.svg'`, `'photo.bmp'`, `'me.tiff'`, `'avatar.avif'`, `'photo'` (no extension), `'photo.PDF'`, or `'icon.ico'`. The `extname` is lowercased and looked up in the mime map; misses throw this error.","commonSituations":"User has an SVG/BMP/TIFF/AVIF/HEIC avatar; file with no extension; wrong extension (`.pdf`, `.heic` from a phone); case differences are handled but the extension itself must be in the set.","solutions":["Convert the photo to PNG, JPEG, WebP, or GIF and use the matching extension.","Rename mislabeled files (`.jpgg` → `.jpg`) — but only if the bytes actually match.","For SVG/AVIF/HEIC, rasterize to PNG/WebP first with an image tool (`cwebp`, `convert`, or browser canvas).","If you genuinely need a new format, extend `PHOTO_MIME_BY_EXT` and ensure the template/CSS handle it.","Validate upstream: `if (!/\\.(png|jpe?g|webp|gif)$/i.test(photo)) throw ...`."],"exampleFix":"# before\nphoto: 'avatar.svg'  # throws\n# after — rasterize once\nconvert avatar.svg avatar.png\n# then in config\nphoto: 'avatar.png'","handlingStrategy":"validation","validationCode":"const ALLOWED = /^(.+\\.(png|jpe?g|webp|gif))$/i;\nfunction validPhotoFile(p) {\n  if (!ALLOWED.test(String(p))) throw new Error('photo must be .png/.jpg/.jpeg/.webp/.gif');\n  return p;\n}","typeGuard":"function isAcceptablePhotoFile(p) {\n  return /\\.(png|jpe?g|webp|gif)$/i.test(String(p ?? ''));\n}","tryCatchPattern":null,"preventionTips":["Convert avatars to PNG/JPEG/WebP/GIF before referencing them.","Reject SVG/BMP/TIFF/AVIF/HEIC at the upload boundary.","Rasterize SVG with `cwebp`/`convert`/canvas before embedding.","Extend PHOTO_MIME_BY_EXT intentionally if a new format is required."],"tags":["validation","photo","file-format","build-cv-html"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}