{"record":{"id":"2bffa842452590da","repo":"actualbudget/actual","slug":"font-file-exceeds-maximum-size-of-max-font-file","errorCode":null,"errorMessage":"Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.","messagePattern":"Font file exceeds maximum size of (.+?)MB\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/style/customThemes.ts","lineNumber":283,"sourceCode":"\n  for (const block of fontFaceBlocks) {\n    urlRegex.lastIndex = 0;\n    let match;\n    while ((match = urlRegex.exec(block)) !== null) {\n      const uri = (match[1] ?? match[2] ?? match[3]).trim();\n      if (!uri.startsWith('data:')) {\n        throw new Error(\n          'Invalid font src: only data: URIs are allowed in @font-face. ' +\n            'Remote URLs (http/https) are not permitted to protect user privacy. ' +\n            'Font files are automatically embedded when installing from GitHub.',\n        );\n      }\n      // Estimate decoded size from base64 content\n      const base64Match = uri.match(/;base64,(.+)$/);\n      if (base64Match) {\n        const size = Math.ceil((base64Match[1].length * 3) / 4);\n        if (size > MAX_FONT_FILE_SIZE) {\n          throw new Error(\n            `Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.`,\n          );\n        }\n        totalSize += size;\n      }\n    }\n  }\n\n  if (totalSize > MAX_TOTAL_FONT_SIZE) {\n    throw new Error(\n      `Total embedded font data exceeds maximum of ${MAX_TOTAL_FONT_SIZE / 1024 / 1024}MB.`,\n    );\n  }\n}\n\n/**\n * Split CSS declarations by semicolons, but respect quoted strings and url() contents.\n * This is needed because data: URIs contain semicolons (e.g., \"data:font/woff2;base64,...\").","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/style/customThemes.ts#L265-L301","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nsrc: url(data:font/ttf;base64,<8MB full font>) format('truetype');\n// after\nsrc: url(data:font/woff2;base64,<subset woff2 under the size limit>) format('woff2');","handlingStrategy":"validation","validationCode":"function checkFontSizes(css, maxBytes) {\n  for (const m of css.matchAll(/;base64,([A-Za-z0-9+/=]+)/g)) {\n    if (Math.ceil(m[1].length * 3 / 4) > maxBytes) return false;\n  }\n  return true;\n}","typeGuard":null,"tryCatchPattern":"try {\n  await installTheme(css);\n} catch (err) {\n  if ((err as Error).message.includes('Font file exceeds maximum size')) {\n    // ask the theme author to subset/compress the font\n  } else throw err;\n}","preventionTips":["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."],"tags":["css","font-face","size-limit","validation"],"backgroundTag":"payload-size-limit-exceeded","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}