Skip to content

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

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

Labels

Fix rolling outWe start the rollout with the fix for this issue.bugSomething isn't working

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