Skip to content

fix(static): compressed public assets never actually served (only bundled) - #4501

Closed
lxtzfr wants to merge 1 commit into
nitrojs:mainfrom
lxtzfr:fix/static-asset-encoding-selection
Closed

fix(static): compressed public assets never actually served (only bundled)#4501
lxtzfr wants to merge 1 commit into
nitrojs:mainfrom
lxtzfr:fix/static-asset-encoding-selection

Conversation

@lxtzfr

@lxtzfr lxtzfr commented Jul 31, 2026

Copy link
Copy Markdown

Summary

  • compressPublicAssets correctly generates .gz/.br/.zst sibling files at build time, and the manifest correctly records their encoding.
  • But runtime/internal/static.ts's encoding-negotiation loop has a bug: the break inside the inner for only exits that inner loop, not the outer for (const encoding of encodings) loop. Since encodings always ends with "" (the uncompressed variant, which always exists), the outer loop keeps going after finding a compressed match and the uncompressed asset silently overwrites asset on the last iteration.
  • Net effect: pre-compressed assets are built and shipped in the output, but the server never serves them — every request gets the full uncompressed asset regardless of Accept-Encoding, silently (no error, Vary: Accept-Encoding header still present, just always the wrong body).

Repro

// nitro.config.ts
export default defineConfig({
  compressPublicAssets: { gzip: true, brotli: true },
});

Build, then:

curl -sD - -H "Accept-Encoding: gzip, br" http://localhost:3000/some-large-asset.css -o /dev/null

Response is always the full uncompressed Content-Length, with no Content-Encoding header.

Fix

Label the outer loop and break out of both loops once a match is found, instead of only the inner one. One-line change, verified locally (built, confirmed Content-Encoding: br + correct compressed Content-Length after the fix, both missing before it).

… is found

The inner loop's break only exited the innermost for-of, so after finding
a matching compressed asset (e.g. .br or .gz) the outer loop kept iterating
and re-checked the next encoding candidate — which always includes the
empty (uncompressed) suffix last. That uncompressed variant always exists
too, so it silently overwrote `asset`, meaning compressPublicAssets-built
.gz/.br files were generated but never actually served, regardless of the
client's Accept-Encoding header.

Repro: build with compressPublicAssets: { gzip: true, brotli: true }, then
request any public asset with `Accept-Encoding: gzip` — the response is
always the full uncompressed size with no Content-Encoding header.
@lxtzfr
lxtzfr requested a review from pi0 as a code owner July 31, 2026 19:39
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

@lxtzfr is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 44c7939d-12d9-4be1-a8b2-d9acf3009db2

📥 Commits

Reviewing files that changed from the base of the PR and between 77b77ff and 02841bd.

📒 Files selected for processing (1)
  • src/runtime/internal/static.ts

📝 Walkthrough

Walkthrough

The static asset lookup now exits both nested encoding and path loops after it finds a matching asset.

Changes

Asset lookup

Layer / File(s) Summary
Stop candidate search after asset match
src/runtime/internal/static.ts
A labeled outer-loop break stops further encoding and path candidate checks after asset selection.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Possibly related PRs

  • nitrojs/nitro#4470: Both changes update asset-selection loop control in src/runtime/internal/static.ts.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits syntax and accurately describes the static asset serving fix.
Description check ✅ Passed The description clearly explains the bug, reproduction steps, impact, and fix, and it matches the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lxtzfr

lxtzfr commented Jul 31, 2026

Copy link
Copy Markdown
Author

Closing — I mis-tested this. My repro was contaminated by a stale local dev-server process that was serving an older, actually-broken build; once properly killed and re-tested cleanly, the current code on main handles this correctly (the id = _id reassignment on match means a subsequent outer-loop iteration looks up a path that no longer exists in the manifest, so it can't silently overwrite an already-found compressed asset). Sorry for the noise!

@lxtzfr lxtzfr closed this Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant