Skip to content

Commit ca5d5ec

Browse files
authored
fix: prevent endless SPA 404 loop (#11354)
1 parent 87da73e commit ca5d5ec

File tree

17 files changed

+167
-28
lines changed

17 files changed

+167
-28
lines changed

.changeset/angry-lamps-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@sveltejs/kit": patch
3+
---
4+
5+
fix: prevent endless SPA 404 loop

packages/kit/src/runtime/client/client.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,16 +2118,21 @@ async function load_data(url, invalid) {
21182118

21192119
const res = await native_fetch(data_url.href);
21202120

2121-
// if `__data.json` doesn't exist or the server has an internal error,
2122-
// fallback to native navigation so we avoid parsing the HTML error page as a JSON
2123-
if (res.headers.get('content-type')?.includes('text/html')) {
2124-
await native_navigation(url);
2125-
}
2126-
21272121
if (!res.ok) {
21282122
// error message is a JSON-stringified string which devalue can't handle at the top level
21292123
// turn it into a HttpError to not call handleError on the client again (was already handled on the server)
2130-
throw new HttpError(res.status, await res.json());
2124+
// if `__data.json` doesn't exist or the server has an internal error,
2125+
// avoid parsing the HTML error page as a JSON
2126+
/** @type {string | undefined} */
2127+
let message;
2128+
if (res.headers.get('content-type')?.includes('application/json')) {
2129+
message = await res.json();
2130+
} else if (res.status === 404) {
2131+
message = 'Not Found';
2132+
} else if (res.status === 500) {
2133+
message = 'Internal Error';
2134+
}
2135+
throw new HttpError(res.status, message);
21312136
}
21322137

21332138
// TODO: fix eslint error / figure out if it actually applies to our situation

packages/kit/src/runtime/control.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class Redirect {
3333
/**
3434
* An error that was thrown from within the SvelteKit runtime that is not fatal and doesn't result in a 500, such as a 404.
3535
* `SvelteKitError` goes through `handleError`.
36+
* @extends Error
3637
*/
3738
export class SvelteKitError extends Error {
3839
/**

packages/kit/src/runtime/server/page/respond_with_error.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export async function respond_with_error({
8686
state,
8787
page_config: {
8888
ssr,
89-
csr: get_option([default_layout], 'csr') ?? true
89+
csr
9090
},
9191
status,
9292
error: await handle_error_and_jsonify(event, options, error),
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "test-no-ssr",
3+
"private": true,
4+
"version": "0.0.1",
5+
"scripts": {
6+
"dev": "vite dev",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"check": "svelte-kit sync && tsc && svelte-check",
10+
"test": "pnpm test:dev && pnpm test:build",
11+
"test:dev": "cross-env DEV=true playwright test",
12+
"test:build": "playwright test"
13+
},
14+
"devDependencies": {
15+
"@sveltejs/kit": "workspace:^",
16+
"@sveltejs/vite-plugin-svelte": "^3.0.1",
17+
"cross-env": "^7.0.3",
18+
"svelte": "^4.2.8",
19+
"svelte-check": "^3.6.2",
20+
"typescript": "^5.3.3",
21+
"vite": "^5.0.8"
22+
},
23+
"type": "module"
24+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { config as default } from '../../utils.js';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<link rel="icon" type="image/png" href="%sveltekit.assets%/favicon.png" />
7+
%sveltekit.head%
8+
</head>
9+
<body>
10+
<div style="display: contents">%sveltekit.body%</div>
11+
</body>
12+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const ssr = false;

packages/kit/test/apps/no-ssr/src/routes/+layout.server.js

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import { setup } from '../../../../setup.js';
3+
4+
setup();
5+
</script>
6+
7+
<slot />

packages/kit/test/apps/no-ssr/src/routes/+page.svelte

Whitespace-only changes.
1.53 KB
Loading
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/** @type {import('@sveltejs/kit').Config} */
2+
const config = {
3+
kit: {}
4+
};
5+
6+
export default config;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect } from '@playwright/test';
2+
import { test } from '../../../utils.js';
3+
4+
/** @typedef {import('@playwright/test').Response} Response */
5+
6+
test.skip(({ javaScriptEnabled }) => !javaScriptEnabled);
7+
8+
test.describe.configure({ mode: 'parallel' });
9+
10+
test('navigating to a non-existent route renders the default error page', async ({ page }) => {
11+
test.setTimeout(3000);
12+
await page.goto('/non-existent-route');
13+
await page.waitForLoadState('networkidle');
14+
expect(await page.textContent('h1')).toBe('404');
15+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"checkJs": true,
5+
"esModuleInterop": true,
6+
"noEmit": true,
7+
"paths": {
8+
"@sveltejs/kit": ["../../../types"],
9+
"types": ["../../../types/internal"]
10+
},
11+
"resolveJsonModule": true
12+
},
13+
"extends": "./.svelte-kit/tsconfig.json"
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as path from 'node:path';
2+
import { sveltekit } from '@sveltejs/kit/vite';
3+
4+
/** @type {import('vite').UserConfig} */
5+
const config = {
6+
build: {
7+
minify: false
8+
},
9+
clearScreen: false,
10+
plugins: [sveltekit()],
11+
server: {
12+
fs: {
13+
allow: [path.resolve('../../../src')]
14+
}
15+
}
16+
};
17+
18+
export default config;

pnpm-lock.yaml

Lines changed: 50 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)