actualbudget/actual · error

Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 102

Error message

Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.

What it means

validateFontFaceBlocks estimates the decoded size of each embedded base64 font and throws if a single font exceeds MAX_FONT_FILE_SIZE (the limit in MB appears in the message). This keeps malicious or bloated themes from embedding enormous payloads in @font-face data URIs.

Source

Thrown at packages/desktop-client/src/style/customThemes.ts:283

  for (const block of fontFaceBlocks) {
    urlRegex.lastIndex = 0;
    let match;
    while ((match = urlRegex.exec(block)) !== null) {
      const uri = (match[1] ?? match[2] ?? match[3]).trim();
      if (!uri.startsWith('data:')) {
        throw new Error(
          'Invalid font src: only data: URIs are allowed in @font-face. ' +
            'Remote URLs (http/https) are not permitted to protect user privacy. ' +
            'Font files are automatically embedded when installing from GitHub.',
        );
      }
      // Estimate decoded size from base64 content
      const base64Match = uri.match(/;base64,(.+)$/);
      if (base64Match) {
        const size = Math.ceil((base64Match[1].length * 3) / 4);
        if (size > MAX_FONT_FILE_SIZE) {
          throw new Error(
            `Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.`,
          );
        }
        totalSize += size;
      }
    }
  }

  if (totalSize > MAX_TOTAL_FONT_SIZE) {
    throw new Error(
      `Total embedded font data exceeds maximum of ${MAX_TOTAL_FONT_SIZE / 1024 / 1024}MB.`,
    );
  }
}

/**
 * Split CSS declarations by semicolons, but respect quoted strings and url() contents.
 * This is needed because data: URIs contain semicolons (e.g., "data:font/woff2;base64,...").

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Subset the font to the glyphs actually needed (e.g. with pyftsubset/glyphhanger) and re-embed.
  2. Convert the font to WOFF2 compression before embedding to shrink the base64 payload.
  3. Split a variable font into only the weights actually used.
  4. Remove the @font-face block and reference a locally installed font instead.

Example fix

// before
src: url(data:font/ttf;base64,<8MB full font>) format('truetype');
// after
src: url(data:font/woff2;base64,<subset woff2 under the size limit>) format('woff2');
Defensive patterns

Strategy: validation

Validate before calling

function checkFontSizes(css, maxBytes) {
  for (const m of css.matchAll(/;base64,([A-Za-z0-9+/=]+)/g)) {
    if (Math.ceil(m[1].length * 3 / 4) > maxBytes) return false;
  }
  return true;
}

Try / catch

try {
  await installTheme(css);
} catch (err) {
  if ((err as Error).message.includes('Font file exceeds maximum size')) {
    // ask the theme author to subset/compress the font
  } else throw err;
}

Prevention

When it happens

Trigger: A theme's actual.css contains an @font-face with a data: URI whose base64 payload decodes to more than MAX_FONT_FILE_SIZE bytes (base64 length * 3/4).

Common situations: Theme author embedded a full multi-weight CJK or variable font family as one giant base64 blob; fonts embedded without subsetting; accidentally inlining a TTF instead of a compressed WOFF2.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/2bffa842452590da. Report an issue: GitHub.