Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-trailing-slash-error-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: preserve trailingSlash config when error boundary is above page node
24 changes: 20 additions & 4 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -635,9 +635,9 @@ async function initialize(result, target, hydrate) {
* form?: Record<string, any> | null;
* }} opts
*/
function get_navigation_result_from_branch({ url, params, branch, status, error, route, form }) {
function get_navigation_result_from_branch({ url, params, branch, status, error, route, form, trailing_slash }) {
Copy link
Contributor

@vercel vercel bot Feb 20, 2026

Choose a reason for hiding this comment

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

JSDoc type definition for get_navigation_result_from_branch is missing the trailing_slash parameter, causing TypeScript errors

Fix on Vercel

/** @type {import('types').TrailingSlash} */
let slash = 'never';
let slash = trailing_slash ?? 'never';

// if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of
// the `trailingSlash` route option, so that relative paths to JS and CSS work
Expand Down Expand Up @@ -1197,13 +1197,21 @@ async function load_route({ id, invalidating, url, params, route, preload }) {

const error_load = await load_nearest_error_page(i, branch, errors);
if (error_load) {
// preserve trailingSlash config from the full branch so that
// slicing the branch for error handling doesn't lose it (#13516)
let trailing_slash;
for (const node of branch) {
if (node?.slash !== undefined) trailing_slash = node.slash;
}
Comment on lines +1200 to +1205
Copy link
Member

Choose a reason for hiding this comment

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

It's probably better to create a util for this so we don't repeat it so many times. It's currently repeated 3 times in the module.

Maybe something like:

/**
 * @param {(import('./types.js').BranchNode | undefined)[]} branch
 * @param {URL} url
 * @returns {import('types').TrailingSlash}
 */
function get_trailing_slash(branch, url) {
	// if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of
	// the `trailingSlash` route option, so that relative paths to JS and CSS work
	if (base && (url.pathname === base || url.pathname === base + '/')) {
		return 'always';
	}

	return branch.reduce((value, node) => {
		return node?.slash ?? value;
	}, /** @type {import('types').TrailingSlash} */ ('never'));
}


return get_navigation_result_from_branch({
url,
params,
branch: branch.slice(0, error_load.idx).concat(error_load.node),
status,
error,
route
route,
trailing_slash
});
} else {
return await server_fallback(url, { id: route.id }, error, status);
Expand Down Expand Up @@ -2401,13 +2409,21 @@ export async function set_nearest_error_page(error, status = 500) {

const error_load = await load_nearest_error_page(current.branch.length, branch, route.errors);
if (error_load) {
// preserve trailingSlash config from the full branch so that
// slicing the branch for error handling doesn't lose it (#13516)
let trailing_slash;
for (const node of branch) {
if (node?.slash !== undefined) trailing_slash = node.slash;
}

const navigation_result = get_navigation_result_from_branch({
url,
params: current.params,
branch: branch.slice(0, error_load.idx).concat(error_load.node),
status,
error,
route
route,
trailing_slash
});

current = navigation_result.state;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { error } from '@sveltejs/kit';

export const trailingSlash = 'always';

export function load() {
error(500, 'deliberate error');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p>This should not be visible</p>
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,13 @@ test.describe('Routing', () => {
expect(new URL(page.url()).pathname).toBe('/routing/trailing-slash/never');
await expect(page.locator('p')).toHaveText('/routing/trailing-slash/never');
});

test('trailing slash is preserved when error boundary is above page config', async ({ page }) => {
await page.goto('/routing/trailing-slash/error-boundary/');
// The page throws an error, which triggers the error boundary.
// The URL should still have the trailing slash from the page's trailingSlash='always' config.
expect(new URL(page.url()).pathname).toBe('/routing/trailing-slash/error-boundary/');
});
});

test.describe('Shadow DOM', () => {
Expand Down
Loading