Skip to content

Root route under paths.base always redirects to a trailing slash, ignoring trailingSlash: "never" #17391

Open

Description

@firatciftci

Describe the bug

With paths.base set, a request for the base root (/some-path) always gets a 308 to /some-path/, even when the root layout exports trailingSlash = "never". Every other route follows the option, so the root is the only URL in the app that ends in a slash.

The rule is hard-coded in src/runtime/server/respond.js:

// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
// regardless of the `trailingSlash` route option
if (url.pathname === base || url.pathname === base + '/') {
	trailing_slash = 'always';
} else if (page_nodes) {

Live, with adapter-node:

$ curl -sI https://www.example.com/some-path
HTTP/1.1 308 Permanent Redirect
location: ./some-path/
x-sveltekit-normalize: 1

I'd expect the root to follow the same trailingSlash value as everything else: "never" serves /some-path and redirects /some-path/ to it, "always" keeps the current behavior. The default could stay "always" for the root if changing it would break existing apps, but there's currently no way to opt out at all.

#13310 was closed by #16788, which only fixed how the prerendered root is excluded from Netlify's edge function, and the runtime rule above is unchanged.

I will open a PR to address the issue.

Reproduction

https://github.com/firatciftci/sveltekit-base-root-trailing-slash

A fresh sv create app with two changes:

// vite.config.ts
sveltekit({ paths: { base: "/app" } });
// src/routes/+layout.ts
export const trailingSlash = "never";

Then pnpm build && node build and compare the root with /about:

$ curl -sI http://localhost:3000/app
HTTP/1.1 308 Permanent Redirect
location: ./app/

$ curl -sI http://localhost:3000/app/
HTTP/1.1 200 OK

$ curl -sI http://localhost:3000/app/about
HTTP/1.1 200 OK

$ curl -sI http://localhost:3000/app/about/
HTTP/1.1 308 Permanent Redirect
location: ../about

/about follows trailingSlash: "never", and the root does the opposite.

Logs

System Info

System:
  OS: macOS 27.0
  CPU: (14) arm64 Apple M4 Pro
  Memory: 928.84 MB / 24.00 GB
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 24.21.0 - /Users/firatciftci/.vite-plus/js_runtime/node/24.21.0/bin/node
  Yarn: 4.18.1 - /Users/firatciftci/.vite-plus/bin/yarn
  npm: 11.19.0 - /Users/firatciftci/.vite-plus/js_runtime/node/24.21.0/bin/npm
  pnpm: 12.10.1 - /Users/firatciftci/.vite-plus/bin/pnpm
  bun: 1.4.2 - /Users/firatciftci/.vite-plus/bin/bun
  Deno: 2.9.6 - /opt/homebrew/bin/deno
  Watchman: 2026.07.27.00 - /opt/homebrew/bin/watchman
Browsers:
  Chrome: 154.0.8037.98
  Firefox Developer Edition: 156.0
  Safari: 27.0

Severity

blocking an upgrade

Additional Information

No response

Activity

  1. svelte-triage-bot commented on Oct 9, 2026

    @svelte-triage-bot
    Contributor

    I reproduced the issue, but could not produce a safe, verified fix.

    What I tried

    • Loaded Root route under paths.base always redirects to a trailing slash, ignoring trailingSlash: "never" #17391 and comments through the GitHub connection; read AGENTS.md, CONTRIBUTING.md, and page-options documentation.: Issue is open with no comments. Documentation describes never/always/ignore but does not explain the base-root exception.
    • Cloned https://github.com/firatciftci/sveltekit-base-root-trailing-slash; ran pnpm install, pnpm build, and node build with original locked dependencies.: Installation and build succeeded with Kit 3.0.1, adapter-node 6.0.0, Svelte 5.57.2, and Vite 8.3.3. HEAD /app returned 308 with location ./app/. HEAD /app/about/ returned 308 with location ../about. agent-browser confirmed /app becomes /app/ and renders the root page.
    • Changed the supplied app's Kit dependency and override to file:../../repos/kit/packages/kit, added Vite fs.allow, reinstalled and rebuilt.: Installation and build succeeded. The exact same request and browser scenario reproduced the unwanted /app/ redirect against current source at a06817e.
    • Created triage/kit-17391-base-root-trailing-slash and reduced the report into options-2 with root-layout trailingSlash = 'never' and a request regression test.: DEV=true pnpm exec playwright test --grep 'honors trailingSlash never at the base root' failed in both JS modes: expected 200, received 308. The built-preview run also failed in both modes, receiving 307 from a separate preview root redirect.
    • Searched existing PRs by issue number and symptom; unshallowed history and examined normalization, relative paths, preview serving, and prerender output.: No open addressing PR found. always add trailing slash to base path #9343 deliberately introduced the exception to prevent broken relative URLs in prerendered index.html. fix: prevent incorrect trailing slash redirect for prerendered root page when paths.base is set #10763 and fix: respect trailing slash option when navigating from the root page of the basepath #11388 aligned preview/client behavior. fix: resolve all client paths through one base/assets rule #17056 preserves relative rendering and server-route-resolution paths behind proxies. The issue's cited fix: don't record a trailing slash for a prerendered root page combined with a base path #16472 is closed, not merged, and addresses recorded prerender paths rather than runtime normalization.
    • Inspected actual SSR HTML from the supplied app and evaluated its import URLs and hydration base at slashful versus slashless roots.: Current ./_app/immutable/entry imports resolve correctly under /app/ but incorrectly under /app, dropping the base. The hydration base expression also becomes empty at /app. Prerender output still emits index.html for either root form, so restoring a './app' relative prefix would reintroduce the directory-index bug fixed by PR always add trailing slash to base path #9343.
    • Recorded inquiry and decision in /workspace/notes.txt and stopped before editing product code.: Only the diagnostic layout and failing test remain uncommitted in packages/kit/test/apps/options-2. No product patch, full-suite verification, review, commit, or push was performed.

    Why I stopped
    The bug is confirmed, but removing the respond.js special case is not a safe complete fix. The client router and preview server independently enforce the same slash, while relative SSR links, client asset imports, and hydration-base calculation depend on it. Its original purpose remains active: prerendered roots are directory-index files, and slashless-root relative prefixes break those files when served at /app/ or /app/index.html. Supporting the requested behavior safely needs an explicit policy for slashless SSR roots versus prerendered directory roots, coordinated with rendering/path resolution and prerendered serving/adapter behavior. A server-only removal breaks the supplied app's default relative paths; a universal './app' prefix reintroduces the historical static-output regression; a nonprerender-only exception leaves root options inconsistent across rendering modes. I could not establish a safe, verified change within this session, so stopped without committing or pushing.

  2. teemingc commented on Oct 12, 2026

    @teemingc
    Member

    @dummdidumm was there a reason the base path root page must end in a trailing slash?

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