santifer/career-ops · error · Error

Unsupported profile photo format: ${photo} (expected PNG, JP

Error message

Unsupported profile photo format: ${photo} (expected PNG, JPEG, WebP, or GIF)

What it means

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.

Source

Thrown at build-cv-html.mjs:142

      throw new Error('Unsupported profile photo data URL (expected base64 PNG, JPEG, WebP, or GIF)');
    }
    c.photo = photo;
    return c;
  }

  if (/^https?:\/\//i.test(photo)) {
    c.photo = photo;
    return c;
  }

  if (/^[a-z][a-z0-9+.-]+:/i.test(photo)) {
    throw new Error(`Unsupported profile photo URL scheme: ${photo.split(':', 1)[0]}`);
  }

  const photoPath = isAbsolute(photo) ? photo : resolve(__dirname, photo);
  const mime = PHOTO_MIME_BY_EXT.get(extname(photoPath).toLowerCase());
  if (!mime) {
    throw new Error(`Unsupported profile photo format: ${photo} (expected PNG, JPEG, WebP, or GIF)`);
  }

  let bytes;
  try {
    bytes = await readFile(photoPath);
  } catch (err) {
    throw new Error(`Profile photo not found or unreadable: ${photo} (${err.code || err.message})`);
  }
  if (bytes.length === 0) {
    throw new Error(`Profile photo is empty: ${photo}`);
  }
  c.photo = `data:${mime};base64,${bytes.toString('base64')}`;
  return c;
}

function joinItems(items) {
  if (Array.isArray(items)) return items.join(', ');
  return typeof items === 'string' ? items : '';

View on GitHub (pinned to 9b17a8ac97)

Solutions

  1. Convert the photo to PNG, JPEG, WebP, or GIF and use the matching extension.
  2. Rename mislabeled files (`.jpgg` → `.jpg`) — but only if the bytes actually match.
  3. For SVG/AVIF/HEIC, rasterize to PNG/WebP first with an image tool (`cwebp`, `convert`, or browser canvas).
  4. If you genuinely need a new format, extend `PHOTO_MIME_BY_EXT` and ensure the template/CSS handle it.
  5. Validate upstream: `if (!/\.(png|jpe?g|webp|gif)$/i.test(photo)) throw ...`.

Example fix

# before
photo: 'avatar.svg'  # throws
# after — rasterize once
convert avatar.svg avatar.png
# then in config
photo: 'avatar.png'
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = /^(.+\.(png|jpe?g|webp|gif))$/i;
function validPhotoFile(p) {
  if (!ALLOWED.test(String(p))) throw new Error('photo must be .png/.jpg/.jpeg/.webp/.gif');
  return p;
}

Type guard

function isAcceptablePhotoFile(p) {
  return /\.(png|jpe?g|webp|gif)$/i.test(String(p ?? ''));
}

Prevention

When it happens

Trigger: 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.

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

Related errors


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/726f4811c24edd9e. Report an issue: GitHub.