Skip to content

fix: prerendered root +server.js endpoints use dynamic file extensions, warn user if not html - #14135

Closed
aewing wants to merge 10 commits into
sveltejs:mainfrom
aewing:fix/prerendered-root-server-endpoint
Closed

fix: prerendered root +server.js endpoints use dynamic file extensions, warn user if not html#14135
aewing wants to merge 10 commits into
sveltejs:mainfrom
aewing:fix/prerendered-root-server-endpoint

Conversation

@aewing

@aewing aewing commented Aug 7, 2025

Copy link
Copy Markdown
Contributor

Summary

Fixes #11087

When a root +server.js endpoint is prerendered with non-HTML content, it was incorrectly saved as index.html due to a fallback in the output_filename function.

The Problem

The issue occurs when:

  1. Someone has a root +server.js (without a +page.svelte)
  2. The endpoint has export const prerender = true
  3. The endpoint returns non-HTML content (like JSON or plain text)

Previously, the code would always default to index.html for the root path, regardless of content type.

The Solution

This PR:

  • Only defaults to index.html for HTML content at the root path
  • Uses content-type headers to determine appropriate file extensions for non-HTML content:
    • application/jsonindex.json
    • text/plainindex.txt
    • Other common types are mapped appropriately
  • Adds a warning when prerendering root +server.js endpoints, since most static hosts expect index.html at the root
  • Falls back to index (no extension) for unrecognized content types

Tests

Added tests to verify:

  • JSON endpoints are saved without .html extension
  • Plain text endpoints are saved without .html extension
  • The fix doesn't break existing functionality

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

Fixes sveltejs#11087

When a root +server.js endpoint is prerendered with non-HTML content,
it was incorrectly saved as 'index.html' due to a fallback in the
output_filename function.

This change:
- Only defaults to 'index.html' for HTML content at the root path
- Uses content-type headers to determine appropriate file extensions
  for non-HTML content (e.g., 'index.json' for JSON, 'index.txt' for text)
- Adds a warning when prerendering root +server.js endpoints since most
  static hosts expect 'index.html' at the root
- Includes tests to verify the fix works correctly
@changeset-bot

changeset-bot Bot commented Aug 7, 2025

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9671daa

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@aewing
aewing marked this pull request as draft August 7, 2025 07:44
@aewing aewing changed the title fix: prerendered root +server.js endpoints use correct file extensions fix: prerendered root +server.js endpoints use dynamic file extensions, warn user Aug 7, 2025
@aewing aewing changed the title fix: prerendered root +server.js endpoints use dynamic file extensions, warn user fix: prerendered root +server.js endpoints use dynamic file extensions, warn user if not html Aug 7, 2025
- Add test for root +server.js endpoint returning JSON (saved as index.json)
- Add test for root +server.js endpoint returning text/plain (saved as index.txt)
- Remove incorrect nested endpoint tests that didn't actually test the root case
- Tests verify the fix correctly generates index.json and index.txt instead of index.html
@aewing

aewing commented Aug 7, 2025

Copy link
Copy Markdown
Contributor Author

@eltigerchino thoughts on this approach?

Comment thread packages/kit/src/core/postbuild/prerender.js Outdated
@aewing
aewing marked this pull request as ready for review August 7, 2025 08:24
Comment thread .changeset/puny-doodles-taste.md Outdated
Comment on lines +201 to +215
// Common mime type to extension mappings
/** @type {Record<string, string>} */
const mime_to_ext = {
'application/json': '.json',
'text/plain': '.txt',
'text/css': '.css',
'text/javascript': '.js',
'application/javascript': '.js',
'application/xml': '.xml',
'text/xml': '.xml',
'application/pdf': '.pdf',
'image/svg+xml': '.svg'
};

return mime_to_ext[mime] || '';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We can probably use the mrmime package that's already installed to do this. So something like:

import { mimes } from 'mrmime';

// TODO: `mimes` is a map of file extensions to mime types but we want it the other way around

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I actually looked into this, but from what I recall mrmime doesn't export mime -> ext it's ext -> mime, so then we have to dedupe in some cases (jpeg, jpg) -- I felt like this was a safer API because we could easily reason about our commitments and support this way -- but happy to reverse-map the mrmime package and go with first/last match if that's what we'd prefer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

jpeg/jpg is a weird example but you get my meaning


// Special handling for root path
if (!file) {
if (is_html) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking at how is_html is evaluated, I'm wondering why a redirect response automatically considers the file as a html file. We might need to rethink that one.

Comment on lines +179 to +182
if (content_type) {
const ext = get_extension_from_content_type(content_type);
return ext ? `index${ext}` : 'index';
}

@teemingc teemingc Aug 7, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if we need this logic for non-root paths as well. We're already appending .html to paths missing it if the content-type header is html.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We probably only need one test app for this and it could be used for edge cases like this one where there is no root page. (So we might rename this to prerendering/no-root-page , etc.)

aewing and others added 2 commits August 7, 2025 02:23
improved language per @eltigerchino

Co-authored-by: Tee Ming <chewteeming01@gmail.com>
…ypes

- Apply content-type based extensions to non-root paths (e.g., /json -> json/index.json)
- Handle redirects from paths with extensions better (e.g., /data.json redirect -> data.json/index.html)
- Use mrmime for mime type to extension mapping with simple heuristic for common types
- Consolidate root-server-text and root-server-endpoint tests into no-root-page test
@Rich-Harris

Copy link
Copy Markdown
Member

This... doesn't seem like a good idea to me at all. The point of a root route is that you can access it by visiting /, but in this case you're supposed to visit / in development but /index.json (or whatever) in production? That makes no sense. What am I missing?

Combine that with all the extra complexity this PR entails, and the fact that we have to add a whole new test app (which will slow down every CI run) it's a hard 👎 from me. I would accept a PR that threw an error if a prerenderable root +server.js responded with a non-HTML response.

@aewing

aewing commented Aug 7, 2025

Copy link
Copy Markdown
Contributor Author

This... doesn't seem like a good idea to me at all. The point of a root route is that you can access it by visiting /, but in this case you're supposed to visit / in development but /index.json (or whatever) in production? That makes no sense. What am I missing?

Combine that with all the extra complexity this PR entails, and the fact that we have to add a whole new test app (which will slow down every CI run) it's a hard 👎 from me. I would accept a PR that threw an error if a prerenderable root +server.js responded with a non-HTML response.

Yea, this one felt a bit off to me.
I will leave this open in case anyone wants to make an argument in favor of this solution, but I will open an alternative where I throw an error instead of the warning I had initially and leave it at that.

@teemingc

teemingc commented Aug 8, 2025

Copy link
Copy Markdown
Member

This... doesn't seem like a good idea to me at all. The point of a root route is that you can access it by visiting /, but in this case you're supposed to visit / in development but /index.json (or whatever) in production? That makes no sense. What am I missing?

Combine that with all the extra complexity this PR entails, and the fact that we have to add a whole new test app (which will slow down every CI run) it's a hard 👎 from me. I would accept a PR that threw an error if a prerenderable root +server.js responded with a non-HTML response.

I'm fine with that. It doesn't seem like this was a problem anyone ran into since the issue had very few responses. I also can't remember where that original discussion was that made me file the issue in the first place.

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.

Prerendered root +server.js is always saved as index.html

3 participants