Skip to content

native-federation dev-server serves binary artifacts (fonts/images) as utf-8, corrupting them #1119

Description

@mohamedatia88-gm

Summary

The Native Federation dev-server fallback middleware reads every served artifact with fs.readFileSync(fileName, 'utf-8'). For binary artifacts emitted into the build output (e.g. @fontsource/* fonts bundled into /media/*.woff2 / *.woff), decoding as UTF-8 replaces every non-UTF-8 byte with U+FFFD and then re-encodes on res.end(). The bytes are both corrupted and inflated, while the file's own internal length header is unchanged, so the browser rejects the asset.

Browser symptoms (Chromium):

Failed to decode downloaded font: http://localhost:7201/media/inter-latin-400-normal-XXXX.woff2
OTS parsing error: Failed to convert WOFF 2.0 font to SFNT
Failed to decode downloaded font: http://localhost:7201/media/inter-latin-400-normal-YYYY.woff
OTS parsing error: incorrect file size in WOFF header

Observed on @angular-architects/native-federation v21.2.3 (Angular 21).

Root cause

src/builders/build/builder.js, dev-server middleware:

const lookup = mrmime.lookup;
const mimeType = lookup(path.extname(fileName)) || 'text/javascript';
const rawBody = fs.readFileSync(fileName, 'utf-8');   // <-- corrupts binaries
const body = rawBody;
res.writeHead(200, { 'Content-Type': mimeType, /* CORS… */ });
res.end(body);

mrmime.lookup already returns the correct binary MIME (font/woff2), so the only defect is the 'utf-8' encoding argument forcing a string round-trip.

Evidence (byte-level)

On-disk source inter-latin-400-normal.woff2 is 23664 bytes; the dev server returns 42613 bytes for the same URL. Diffing the bytes:

disk   @0x30:  1a 81 50 1b 81 af 46 1c d5 70 …
served @0x30:  1a efbfbd 50 1b efbfbd efbfbd 46 1c efbfbd 70 …

ef bf bd is UTF-8 for U+FFFD (replacement character): every byte ≥ 0x80 that is not valid UTF-8 has been replaced and re-encoded. The leading ASCII-range bytes (the wOF2 header) pass through unchanged, which is why only the body is mangled and the declared length no longer matches the payload.

Reproduction

  1. Angular app served via @angular-architects/native-federation:build (serve-federation).
  2. Bundle any binary asset into the build output — e.g. add @fontsource/inter CSS to angular.json … styles[] so esbuild emits /media/*.woff2.
  3. Load the app; observe the OTS errors above. curl the font URL and compare its byte length to the on-disk file — the served file is ~1.8× larger.

Only triggers when the middleware is the handler (i.e. the artifact exists under the dev-server output path); JS/JSON artifacts survive UTF-8 round-tripping, so the corruption is invisible until a binary asset is served this way.

Fix

Read as a Buffer (omit the encoding) so raw bytes pass through untouched:

const rawBody = fs.readFileSync(fileName);   // Buffer, not string

res.end(buffer) then writes the exact bytes. Verified: served output becomes byte-identical to the source (23664 / 30696 bytes), correct Content-Type, fonts decode.

Notes

  • Production is unaffected — the static host (e.g. nginx) serves /media correctly; this is strictly a dev-server defect.
  • Other binary artifacts that can land in the output (bundled images, source maps served as downloads, etc.) are corrupted the same way.

I'm happy to open a PR with the one-line change if that's helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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