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
- Subset the font to the glyphs actually needed (e.g. with pyftsubset/glyphhanger) and re-embed.
- Convert the font to WOFF2 compression before embedding to shrink the base64 payload.
- Split a variable font into only the weights actually used.
- 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
- Subset fonts to used glyphs with pyftsubset before embedding.
- Prefer WOFF2 over TTF/OTF for embedded fonts.
- Check base64 payload sizes in CI before publishing a theme.
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
- Total embedded font data exceeds maximum of ${MAX_TOTAL_FONT
- Invalid font src: only data: URIs are allowed in @font-face.
- Invalid font-family value for "${property}": value must not
- Invalid font-family value for "${property}": empty font name
- Invalid font-family value for "${property}": function calls
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/2bffa842452590da.
Report an issue: GitHub.