Skip to content

Commit 2c5a0cf

Browse files
authored
[fix] break out of 404 SPA loop (#6918)
* wip * tidy up * remove return to let client.js handle the distinction - previously this resulted in the message always being "Internal Error", for 404s, too * fix status codes * fix error to App.Error transformation fallback + pass eventId on client * correct event and error passed to handle_error
1 parent 842c08b commit 2c5a0cf

File tree

5 files changed

+57
-44
lines changed

5 files changed

+57
-44
lines changed

.changeset/selfish-geckos-grin.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] handle SPA root data loading error

packages/kit/src/core/sync/write_client_manifest.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export function write_client_manifest(config, manifest_data, output) {
9999
export const hooks = {
100100
handleError: ${
101101
hooks_file ? 'client_hooks.handleError || ' : ''
102-
}(({ error }) => { console.error(error); return { message: 'Internal Error' }; }),
102+
}(({ error }) => { console.error(error) }),
103103
};
104104
`)
105105
);

packages/kit/src/exports/vite/build/build_server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export class Server {
7474
get request() {
7575
throw new Error('request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details');
7676
}
77-
}) ?? { message: event.routeId ? 'Internal Error' : 'Not Found' };
77+
}) ?? { message: event.routeId != null ? 'Internal Error' : 'Not Found' };
7878
},
7979
hooks: null,
8080
manifest,

packages/kit/src/exports/vite/dev/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ export async function dev(vite, vite_config, svelte_config) {
432432
'request in handleError has been replaced with event. See https://github.com/sveltejs/kit/pull/3384 for details'
433433
);
434434
}
435-
}) ?? { message: event.routeId ? 'Internal Error' : 'Not Found' }
435+
}) ?? { message: event.routeId != null ? 'Internal Error' : 'Not Found' }
436436
);
437437
},
438438
hooks,

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

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { onMount, tick } from 'svelte';
2-
import { normalize_error } from '../../utils/error.js';
32
import { make_trackable, decode_params, normalize_path } from '../../utils/url.js';
43
import { find_anchor, get_base_uri, scroll_state } from './utils.js';
54
import { lock_fetch, unlock_fetch, initial_fetch, subsequent_fetch } from './fetcher.js';
@@ -92,6 +91,8 @@ export function create_client({ target, base, trailing_slash }) {
9291
url: null
9392
};
9493

94+
/** this being true means we SSR'd */
95+
let hydrated = false;
9596
let started = false;
9697
let autoscroll = true;
9798
let updating = false;
@@ -211,27 +212,13 @@ export function create_client({ target, base, trailing_slash }) {
211212
token = nav_token;
212213
let navigation_result = intent && (await load_route(intent));
213214

214-
if (
215-
!navigation_result &&
216-
url.origin === location.origin &&
217-
url.pathname === location.pathname
218-
) {
219-
// this could happen in SPA fallback mode if the user navigated to
220-
// `/non-existent-page`. if we fall back to reloading the page, it
221-
// will create an infinite loop. so whereas we normally handle
222-
// unknown routes by going to the server, in this special case
223-
// we render a client-side error page instead
224-
navigation_result = await load_root_error_page({
225-
status: 404,
226-
error: new Error(`Not found: ${url.pathname}`),
227-
url,
228-
routeId: null
229-
});
230-
}
231-
232215
if (!navigation_result) {
233-
await native_navigation(url);
234-
return false; // unnecessary, but TypeScript prefers it this way
216+
navigation_result = await server_fallback(
217+
url,
218+
null,
219+
handle_error(new Error(`Not found: ${url.pathname}`), { url, params: {}, routeId: null }),
220+
404
221+
);
235222
}
236223

237224
// if this is an internal navigation intent, use the normalized
@@ -245,7 +232,7 @@ export function create_client({ target, base, trailing_slash }) {
245232
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
246233
navigation_result = await load_root_error_page({
247234
status: 500,
248-
error: new Error('Redirect loop'),
235+
error: handle_error(new Error('Redirect loop'), { url, params: {}, routeId: null }),
249236
url,
250237
routeId: null
251238
});
@@ -722,7 +709,7 @@ export function create_client({ target, base, trailing_slash }) {
722709
} catch (error) {
723710
return load_root_error_page({
724711
status: 500,
725-
error: /** @type {Error} */ (error),
712+
error: handle_error(error, { url, params, routeId: route.id }),
726713
url,
727714
routeId: route.id
728715
});
@@ -827,8 +814,7 @@ export function create_client({ target, base, trailing_slash }) {
827814
} else {
828815
// if we get here, it's because the root `load` function failed,
829816
// and we need to fall back to the server
830-
await native_navigation(url);
831-
return;
817+
return await server_fallback(url, route.id, error, status);
832818
}
833819
}
834820
} else {
@@ -882,7 +868,7 @@ export function create_client({ target, base, trailing_slash }) {
882868
/**
883869
* @param {{
884870
* status: number;
885-
* error: HttpError | Error;
871+
* error: App.Error;
886872
* url: URL;
887873
* routeId: string | null
888874
* }} opts
@@ -912,11 +898,11 @@ export function create_client({ target, base, trailing_slash }) {
912898

913899
server_data_node = server_data.nodes[0] ?? null;
914900
} catch {
915-
// at this point we have no choice but to fall back to the server
916-
await native_navigation(url);
917-
918-
// @ts-expect-error
919-
return;
901+
// at this point we have no choice but to fall back to the server, if it wouldn't
902+
// bring us right back here, turning this into an endless loop
903+
if (url.origin !== location.origin || url.pathname !== location.pathname || hydrated) {
904+
await native_navigation(url);
905+
}
920906
}
921907
}
922908

@@ -943,10 +929,7 @@ export function create_client({ target, base, trailing_slash }) {
943929
params,
944930
branch: [root_layout, root_error],
945931
status,
946-
error:
947-
error instanceof HttpError
948-
? error.body
949-
: handle_error(error, { url, params, routeId: null }),
932+
error,
950933
route: null
951934
});
952935
}
@@ -1071,6 +1054,28 @@ export function create_client({ target, base, trailing_slash }) {
10711054
);
10721055
}
10731056

1057+
/**
1058+
* Does a full page reload if it wouldn't result in an endless loop in the SPA case
1059+
* @param {URL} url
1060+
* @param {string | null} routeId
1061+
* @param {App.Error} error
1062+
* @param {number} status
1063+
* @returns {Promise<import('./types').NavigationFinished>}
1064+
*/
1065+
async function server_fallback(url, routeId, error, status) {
1066+
if (url.origin === location.origin && url.pathname === location.pathname && !hydrated) {
1067+
// We would reload the same page we're currently on, which isn't hydrated,
1068+
// which means no SSR, which means we would end up in an endless loop
1069+
return await load_root_error_page({
1070+
status,
1071+
error,
1072+
url,
1073+
routeId
1074+
});
1075+
}
1076+
return await native_navigation(url);
1077+
}
1078+
10741079
/**
10751080
* Loads `href` the old-fashioned way, with a full page reload.
10761081
* Returns a `Promise` that never resolves (to prevent any
@@ -1413,6 +1418,8 @@ export function create_client({ target, base, trailing_slash }) {
14131418
data: server_data_nodes,
14141419
form
14151420
}) => {
1421+
hydrated = true;
1422+
14161423
const url = new URL(location.href);
14171424

14181425
/** @type {import('./types').NavigationFinished | undefined} */
@@ -1447,19 +1454,17 @@ export function create_client({ target, base, trailing_slash }) {
14471454
form,
14481455
route: routes.find((route) => route.id === routeId) ?? null
14491456
});
1450-
} catch (e) {
1451-
const error = normalize_error(e);
1452-
1457+
} catch (error) {
14531458
if (error instanceof Redirect) {
14541459
// this is a real edge case — `load` would need to return
14551460
// a redirect but only in the browser
1456-
await native_navigation(new URL(/** @type {Redirect} */ (e).location, location.href));
1461+
await native_navigation(new URL(error.location, location.href));
14571462
return;
14581463
}
14591464

14601465
result = await load_root_error_page({
14611466
status: error instanceof HttpError ? error.status : 500,
1462-
error,
1467+
error: handle_error(error, { url, params, routeId }),
14631468
url,
14641469
routeId
14651470
});
@@ -1507,9 +1512,12 @@ async function load_data(url, invalid) {
15071512
* @returns {App.Error}
15081513
*/
15091514
function handle_error(error, event) {
1515+
if (error instanceof HttpError) {
1516+
return error.body;
1517+
}
15101518
return (
15111519
hooks.handleError({ error, event }) ??
1512-
/** @type {any} */ ({ message: event.routeId ? 'Internal Error' : 'Not Found' })
1520+
/** @type {any} */ ({ message: event.routeId != null ? 'Internal Error' : 'Not Found' })
15131521
);
15141522
}
15151523

0 commit comments

Comments
 (0)