Skip to content

fix(server): serve static assets with standard MIME types - #2713

Merged
james-elicx merged 5 commits into
cloudflare:mainfrom
Boyeep:fix/static-asset-mime-types
Jul 27, 2026
Merged

fix(server): serve static assets with standard MIME types#2713
james-elicx merged 5 commits into
cloudflare:mainfrom
Boyeep:fix/static-asset-mime-types

Conversation

@Boyeep

@Boyeep Boyeep commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add standard MIME types for common web assets including WebAssembly, media, manifests, and documents
  • match file extensions case-insensitively across cached, filesystem, and image passthrough paths
  • make the existing WASM compression eligibility reachable by emitting application/wasm

Next.js parity

Next.js static serving delegates MIME lookup to its compiled send/mime database. This ports the relevant common web-asset behavior without adding a runtime dependency.

Test plan

  • pnpm test tests/static-file-cache.test.ts tests/serve-static.test.ts (76 passed)
  • staged vp check --fix passed

@pkg-pr-new

pkg-pr-new Bot commented Jul 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2713
npm i https://pkg.pr.new/create-vinext-app@2713
npm i https://pkg.pr.new/@vinext/types@2713
npm i https://pkg.pr.new/vinext@2713

commit: f1c3b17

@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared bdad882 against base 3570928 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 132.4 KB 132.4 KB ⚫ +0.0%
Client entry size (gzip) vinext 119.8 KB 119.8 KB ⚫ +0.0%
Dev server cold start vinext 2.88 s 2.87 s ⚫ -0.4%
Production build time vinext 3.14 s 3.15 s ⚫ +0.3%
RSC entry closure size (gzip) vinext 105.6 KB 105.6 KB ⚫ +0.0%
Server bundle size (gzip) vinext 179.8 KB 179.8 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the MIME-type changes. Overall this is a solid, well-tested improvement: centralizing lookup in contentTypeForPath removes the duplicated path.extname(...).toLowerCase() / ?? "application/octet-stream" pattern in four call sites, the case-insensitive matching is a real fix, and mapping .wasm to application/wasm correctly makes the existing COMPRESSIBLE_TYPES entry reachable. I verified:

  • Security is preserved — image passthrough still gates on isSafeImageContentType / SAFE_IMAGE_CONTENT_TYPES, so the newly-added types (pdf, wasm, mp4, etc.) do NOT loosen the image endpoint.
  • fonts.ts still consumes the exported CONTENT_TYPES map and already lowercases its extension, so it's unaffected.
  • vp check and both test files pass locally (77 passed).

A couple of non-blocking parity observations below. Nothing here blocks merge.

".rsc": "text/x-component",
".woff": "font/woff",
".woff2": "font/woff2",
".xml": "application/xml",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor Next.js-parity nit (non-blocking): Next.js serve-static delegates to sendmime-types, which appends charset whenever mime.charset(type) returns a value. mime-db marks both image/svg+xml and application/xml with charset: UTF-8, so strict parity would serve image/svg+xml; charset=utf-8 and application/xml; charset=utf-8. This PR emits them bare, which is inconsistent with .csv/.txt/.html (which do get the charset). Serving them without charset is harmless in practice, but since the PR's stated goal is matching the send/mime database it's worth either adding the charset to .svg/.xml or noting the deliberate divergence.

".js": "application/javascript; charset=UTF-8",
".mjs": "application/javascript; charset=UTF-8",
".css": "text/css; charset=UTF-8",
".html": "text/html; charset=UTF-8",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit (non-blocking): the map is otherwise alphabetized by extension, but .js, .mjs, .css, .html (lines 26-29) sit out of order between .heic and .ico. Consider moving them to their alphabetical slots so the ordering convention stays consistent and future additions are easy to place.

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #2713.

Summary

I reviewed PR #2713 (serve static assets with standard MIME types) and posted a batch review with a COMMENT verdict — no blocking issues found.

What the PR does well:

  • Centralizes MIME lookup into contentTypeForPath, removing the duplicated path.extname(...).toLowerCase() / ?? "application/octet-stream" pattern across four call sites
  • Adds genuine case-insensitive extension matching (real fix)
  • Correctly maps .wasmapplication/wasm, which makes the pre-existing COMPRESSIBLE_TYPES entry reachable (as the PR body claims)

Verifications I performed:

  • Security preserved: the image passthrough still gates on isSafeImageContentType/SAFE_IMAGE_CONTENT_TYPES, so the newly-added types (pdf, wasm, mp4, etc.) do not widen what the image endpoint will serve.
  • No missed consumers: fonts.ts still uses the exported CONTENT_TYPES and already lowercases its extension, so it's unaffected.
  • vp check passes and both test files pass locally (77 tests).

Two non-blocking nits raised inline:

  1. Parity gap — mime-db marks image/svg+xml and application/xml with charset: UTF-8, so strict send/mime parity would add ; charset=utf-8 to those; the PR emits them bare, inconsistent with .csv/.txt/.html.
  2. Style — .js/.mjs/.css/.html sit out of the otherwise-alphabetical ordering.

github run

@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the MIME-type refactor. The core change is good: contentTypeForPath removes the duplicated path.extname(...).toLowerCase() / ?? "application/octet-stream" pattern at all four call sites, the case-insensitive matching is a real fix, and .wasm -> application/wasm correctly makes the existing COMPRESSIBLE_TYPES entry reachable.

Verified no regressions in adjacent behavior:

  • Compression stays correct — every COMPRESSIBLE_TYPES.has() check first does ct.split(";")[0].trim() (prod-server.ts:497,708,937), so the newly-added charsets don't break compression eligibility.
  • Image security preserved — the passthrough still gates on the SAFE_IMAGE_CONTENT_TYPES allowlist (image-optimization.ts:219) via isSafeImageContentType, which splits on ;. The new non-image types (pdf, wasm, mp4, ...) can't leak through. Note .bmp -> image/bmp newly lets local .bmp images through the image endpoint (previously application/octet-stream -> 400); this matches Next.js's allowlist so it's a parity improvement, not a regression.
  • fonts.ts still reads the exported CONTENT_TYPES for woff2/woff/ttf, which are unaffected.

One issue worth fixing before merge (charset casing), plus a couple of non-blocking nits below.

Comment on lines +22 to +29
".csv": "text/csv; charset=UTF-8",
".eot": "application/vnd.ms-fontobject",
".gif": "image/gif",
".svg": "image/svg+xml",
".heic": "image/heic",
".js": "application/javascript; charset=UTF-8",
".mjs": "application/javascript; charset=UTF-8",
".css": "text/css; charset=UTF-8",
".html": "text/html; charset=UTF-8",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Charset casing regresses an existing value and diverges from the stated send/mime goal.

This PR changes the pre-existing .html entry from text/html; charset=utf-8 (lowercase) to text/html; charset=UTF-8 (uppercase). Every other charset in the codebase is lowercase utf-8 — there are ~28 occurrences (pages-page-response.ts, app-page-stream.ts, http-error-responses.ts, the prod-server 404s at prod-server.ts:1503/2016, etc.). So the newly-served .js/.css/.json assets will carry charset=UTF-8 while HTML page renders and error responses from the same server carry charset=utf-8 — inconsistent on the wire.

It also diverges from the PR's stated parity goal: send derives the charset via mime-types.contentType(), which lowercases it (... + '; charset=' + charset.toLowerCase()), so Next.js emits charset=utf-8. Recommend lowercasing all of these (and reverting the .html change) to restore both internal consistency and send parity:

Suggested change
".csv": "text/csv; charset=UTF-8",
".eot": "application/vnd.ms-fontobject",
".gif": "image/gif",
".svg": "image/svg+xml",
".heic": "image/heic",
".js": "application/javascript; charset=UTF-8",
".mjs": "application/javascript; charset=UTF-8",
".css": "text/css; charset=UTF-8",
".html": "text/html; charset=UTF-8",
".csv": "text/csv; charset=utf-8",
".eot": "application/vnd.ms-fontobject",
".gif": "image/gif",
".heic": "image/heic",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",

(The same lowercasing should be applied to .json/.map/.txt below, and the corresponding test expectations in tests/serve-static.test.ts and tests/static-file-cache.test.ts.)

".pdf": "application/pdf",
".png": "image/png",
".rsc": "text/x-component",
".svg": "image/svg+xml",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity nit (non-blocking): mime-db marks both image/svg+xml and application/xml with charset: UTF-8, so strict send/mime-types parity would emit image/svg+xml; charset=utf-8 and application/xml; charset=utf-8. As written they're bare, which is inconsistent with .csv/.txt/.html in the same map. Either add the charset or note the deliberate divergence. (isSafeImageContentType splits on ;, so adding it wouldn't affect the SVG security gate.)

".gif": "image/gif",
".svg": "image/svg+xml",
".heic": "image/heic",
".js": "application/javascript; charset=UTF-8",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit (non-blocking): the map is otherwise alphabetized, but .js/.mjs/.css/.html (lines 26-29) sit between .heic and .ico out of order. Moving them to their alphabetical slots keeps the convention consistent for future additions.

@ask-bonk

ask-bonk Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully to PR #2713.

Summary

I reviewed the MIME-type refactor and posted a batch review (COMMENT verdict). The refactor itself is solid, but I found one issue worth fixing before merge that the prior review missed.

Main finding — charset casing (charset=UTF-8 vs charset=utf-8):

  • The PR changes the pre-existing .html entry from text/html; charset=utf-8 to text/html; charset=UTF-8 and introduces uppercase UTF-8 on .js/.mjs/.css/.json/.map/.txt/.csv.
  • This is internally inconsistent: the rest of vinext uses lowercase charset=utf-8 in ~28 places (HTML page renders, 404s, dev server, error responses). Static assets would now carry a different casing than page/error responses from the same server.
  • It also diverges from the PR's stated send/mime goal: mime-types.contentType() lowercases the charset, so Next.js emits charset=utf-8. I posted a suggestion to lowercase them and revert the .html change.

Non-blocking nits (posted inline):

  • .svg/.xml are emitted bare, but mime-db marks both charset: UTF-8 — strict parity would add ; charset=utf-8, inconsistent with .csv/.txt/.html.
  • Map ordering: .js/.mjs/.css/.html sit out of the otherwise-alphabetical order.

Verifications that came back clean (no issues):

  • Compression eligibility: all COMPRESSIBLE_TYPES.has() checks split on ; first, so the added charsets don't break it.
  • Image security: passthrough still gates on the SAFE_IMAGE_CONTENT_TYPES allowlist, so new types (pdf, wasm, mp4) can't leak through the image endpoint. .bmp newly passing is a deliberate parity match with Next.js, not a regression.
  • fonts.ts still uses CONTENT_TYPES for woff/ttf (unaffected).

github run

@james-elicx
james-elicx enabled auto-merge (squash) July 27, 2026 14:17
@james-elicx
james-elicx merged commit b779754 into cloudflare:main Jul 27, 2026
55 checks passed
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.

2 participants