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
- Angular app served via
@angular-architects/native-federation:build (serve-federation).
- Bundle any binary asset into the build output — e.g. add
@fontsource/inter CSS to angular.json … styles[] so esbuild emits /media/*.woff2.
- 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.
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 onres.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):
Observed on
@angular-architects/native-federationv21.2.3 (Angular 21).Root cause
src/builders/build/builder.js, dev-server middleware:mrmime.lookupalready 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.woff2is 23664 bytes; the dev server returns 42613 bytes for the same URL. Diffing the bytes:ef bf bdis 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 (thewOF2header) pass through unchanged, which is why only the body is mangled and the declared length no longer matches the payload.Reproduction
@angular-architects/native-federation:build(serve-federation).@fontsource/interCSS toangular.json … styles[]so esbuild emits/media/*.woff2.curlthe 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:res.end(buffer)then writes the exact bytes. Verified: served output becomes byte-identical to the source (23664 / 30696 bytes), correctContent-Type, fonts decode.Notes
/mediacorrectly; this is strictly a dev-server defect.I'm happy to open a PR with the one-line change if that's helpful.