Skip to content

pac code push corrupts all binary assets (fonts, images): files are read and re-encoded as UTF-8 text #374

@5cover

Description

@5cover

pac code push reads every file of the build output with readFile(path, "utf-8") before uploading it. Any binary file (.woff2, .woff, .ttf, binary images, etc.) is therefore decoded as UTF-8 text and re-encoded on upload, which destroys its content: every byte ≥ 0x80 that does not form a valid UTF-8 sequence is replaced/expanded. The corrupted files are then served by the app storage proxy with a correct MIME type and HTTP 200, which makes the failure very hard to diagnose: fonts download "successfully" and then fail OTS parsing in the browser.

Combined with the app host CSP (font-src 'self', which also forbids data: fonts in CSS), this currently makes it impossible to ship a web font in a code app by any documented means.

Environment

  • Power Platform CLI 2.7.4+g06bb2eb (.NET 10.0.7), installed as a dotnet tool on Windows 11
  • Node.js v24.11.1
  • Standard Vite + React code app (created per the "create an app from scratch" quickstart)

Steps to reproduce

  1. Create any Vite code app and add a font file to the build output, e.g. any .woff2 in dist/assets/, referenced from CSS via @font-face { src: url(./assets/my-font.woff2); }.
  2. pnpm build && pac code push (push succeeds).
  3. Open the published app, or fetch the font directly from the app storage proxy:
    https://<env>.environment.api.powerplatformusercontent.com/powerapps/appruntime/<appId>/t/<tenantId>/storageproxy/<version>/assets/my-font.woff2

Expected

The served file is byte-identical to the local dist/assets/my-font.woff2.

Actual

The served file is corrupted. Measured on a real push (Material Icons woff2/woff):

File Local size Served size First differing byte SHA-256 match
material-icons-*.woff2 128 352 B 231 689 B (×1.80) index 10 no
material-icons-*.woff 164 912 B 299 808 B (×1.82) index 10 no

The magic bytes (wOF2/wOFF, pure ASCII) survive; corruption starts at the first byte ≥ 0x80. The ~×1.8 growth is the signature of binary data round-tripped through a UTF-8 text decode/encode. In the browser, the font downloads with HTTP 200 and content-type: font/woff2, then fails with:

Failed to decode downloaded font: https://…/assets/material-icons-….woff2
OTS parsing error: Size of decompressed WOFF 2.0 is less than compressed size
OTS parsing error: incorrect file size in WOFF header

Text assets (.html, .css, .js, .svg) are served byte-identical, since they are valid UTF-8.

Root cause

In the code-apps tooling bundled with the CLI (tools/net10.0/any/Bin.js of microsoft.powerapps.cli.tool 2.7.4), the upload loop reads every file as UTF-8 text (de-minified excerpt):

let D = await vF(V, X), E = V.replace(/^\.\//, ""), S = 0;
for (let o of D) {
  let z1 = await X.readFile(o, "utf-8");          // <-- every file, including binaries
  S += Buffer.byteLength(z1, "utf-8");
  let u1 = o.replace(E, "").replace(/\\/g, "/").replace(/^\//, "");
  let c1 = `${Y}/${u1}${Z}`;
  await W.put(c1, { headers: { "Content-Ty…" } });
}

readFile(path, "utf-8") is lossy for non-UTF-8 content: invalid sequences are replaced, and re-serialization expands high bytes into multi-byte sequences. The upload should read files as Buffer (no encoding) and send the raw bytes.

Impact

  • No web font can be deployed as a file. The host CSP also blocks external fonts (font-src 'self') and data: fonts in CSS, so icon fonts (e.g. Material Icons used by many design systems, including ENGIE's Fluid) silently degrade to ligature text in production while working in local dev.
  • Any other binary asset placed in the build output (raster images, wasm, etc.) is corrupted the same way.
  • The failure is silent: push succeeds, files are served with 200 + correct MIME, and the only symptom is an OTS/decoder error in the browser console.

Workaround (for anyone hitting this)

Ship no binary files at all: inline fonts as base64 inside the JS bundle (text survives the upload) and register them at runtime with document.fonts.add(new FontFace(family, arrayBuffer, descriptors)). Constructing a FontFace from an ArrayBuffer performs no fetch, so font-src does not apply. With Vite: import woff2 from "./font.woff2?inline", decode the data URI with atob, and register on startup.

Suggested fix

In the upload loop, read files without an encoding (await X.readFile(o)) and upload the resulting Buffer unchanged; compute sizes with buffer.length. Alternatively, restrict the UTF-8 path to known text MIME types.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions