Skip to content

adapter-node: prerendered pages are served without a Content-Type #17380

Description

@theescodes

Describe the bug

With @sveltejs/adapter-node 6, prerendered pages are served without a Content-Type header. Static files and client assets still get theirs.

builder.mimeTypes (packages/kit/src/core/adapt/builder.js) collects the extensions it needs from prerendered.paths and the client output:

for (const file of [...prerendered.paths, ...walk(path.join(build_data.out_dir, 'client'))]) {

prerendered.paths are URL paths (/, /about), which have no extension, and the pages' .html files are in the prerendered output, not the client output. So .html only reaches the table when some file in static/ happens to end in .html. Otherwise the static server added in #16908 sends the pages without a Content-Type.

On plain node build this goes unnoticed, because browsers sniff the HTML. With X-Content-Type-Options: nosniff, which is common via Helmet, a proxy or a custom server wrapping handler, Chromium renders every prerendered page as plain text.

The adapter-node test app doesn't catch it: its static/page.html puts .html into the table, which is why the serves prerendered pages and redirects to their canonical path test passes.

Reproduction

From a fresh project:

npx sv create repro --template minimal --types ts --no-add-ons --install npm
cd repro
npx sv add sveltekit-adapter=adapter:node --install npm
echo "export const prerender = true;" > src/routes/+layout.ts
npm run build && PORT=3000 node build
curl -sI http://localhost:3000/ | grep -i content-type            # nothing
curl -sI http://localhost:3000/robots.txt | grep -i content-type  # content-type: text/plain
grep -oE 'mime_types = \{[^}]*\}' build/adapter-node.js
# mime_types = {".txt":"text/plain",".json":"application/json",".js":"text/javascript"}

To see the effect in a browser, serve the same build with nosniff:

// nosniff.js
import { createServer } from 'node:http';
import { handler } from './build/handler.js';

createServer((req, res) => {
	res.setHeader('X-Content-Type-Options', 'nosniff');
	handler(req, res);
}).listen(3001);

node nosniff.js, then open http://localhost:3001/: Chromium shows the page source as text, and document.contentType is text/plain.

Logs

No errors. The pages are a 200 without a content-type header.

System Info

System:
  OS: macOS 27.0.1
Binaries:
  Node: 24.19.0
  npm: 11.17.0
npmPackages:
  @sveltejs/adapter-node: ^6.0.0 => 6.0.0
  @sveltejs/kit: ^3.0.0 => 3.0.1
  @sveltejs/vite-plugin-svelte: ^7.2.0 => 7.3.1
  svelte: ^5.57.1 => 5.57.2
  vite: ^8.3.0 => 8.3.3

Severity

serious, but I can work around it

Additional Information

Also taking the extensions from the pages' files fixes it. Patched into the repro above, the table gains ".html":"text/html" and the pages are served as text/html;charset=utf-8:

for (const file of [
	...prerendered.paths,
	...Array.from(prerendered.pages.values(), (page) => page.file),
	...walk(path.join(build_data.out_dir, 'client'))
]) {

A test app without an .html file in static/ would cover it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions