Skip to content

Commit 11d4a4b

Browse files
Rich-Harrismrkishi
andauthored
Fall back to full page reload if link href doesn't match anything (#3969)
* failing test for #3935 * fall back to full page navigation for unmatched routes - closes #3935 * changeset * fix initial spa render of 404 errors (#3980) Co-authored-by: mrkishi <[email protected]> Co-authored-by: mrkishi <[email protected]>
1 parent dbcbc9a commit 11d4a4b

File tree

7 files changed

+39
-12
lines changed

7 files changed

+39
-12
lines changed

.changeset/four-news-turn.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+
Fall back to full page reload if link href does not match route manifest

packages/kit/src/runtime/app/navigation.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ async function invalidate_(resource) {
4545
/**
4646
* @type {import('$app/navigation').prefetch}
4747
*/
48-
function prefetch_(href) {
49-
return router.prefetch(new URL(href, get_base_uri(document)));
48+
async function prefetch_(href) {
49+
await router.prefetch(new URL(href, get_base_uri(document)));
5050
}
5151

5252
/**

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ export class Renderer {
149149
/** @type {Map<string, import('./types').NavigationResult>} */
150150
this.cache = new Map();
151151

152-
/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult> | null}} */
152+
/** @type {{id: string | null, promise: Promise<import('./types').NavigationResult | undefined> | null}} */
153153
this.loading = {
154154
id: null,
155155
promise: null
@@ -316,6 +316,11 @@ export class Renderer {
316316
const token = (this.token = {});
317317
let navigation_result = await this._get_navigation_result(info, no_cache);
318318

319+
if (!navigation_result) {
320+
location.href = info.url.href;
321+
return;
322+
}
323+
319324
// abort if user navigated during update
320325
if (token !== this.token) return;
321326

@@ -411,7 +416,7 @@ export class Renderer {
411416

412417
/**
413418
* @param {import('./types').NavigationInfo} info
414-
* @returns {Promise<import('./types').NavigationResult>}
419+
* @returns {Promise<import('./types').NavigationResult | undefined>}
415420
*/
416421
load(info) {
417422
this.loading.promise = this._get_navigation_result(info, false);
@@ -471,7 +476,7 @@ export class Renderer {
471476
/**
472477
* @param {import('./types').NavigationInfo} info
473478
* @param {boolean} no_cache
474-
* @returns {Promise<import('./types').NavigationResult>}
479+
* @returns {Promise<import('./types').NavigationResult | undefined>}
475480
*/
476481
async _get_navigation_result(info, no_cache) {
477482
if (this.loading.id === info.id && this.loading.promise) {
@@ -504,11 +509,13 @@ export class Renderer {
504509
if (result) return result;
505510
}
506511

507-
return await this._load_error({
508-
status: 404,
509-
error: new Error(`Not found: ${info.url.pathname}`),
510-
url: info.url
511-
});
512+
if (info.initial) {
513+
return await this._load_error({
514+
status: 404,
515+
error: new Error(`Not found: ${info.url.pathname}`),
516+
url: info.url
517+
});
518+
}
512519
}
513520

514521
/**

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export class Router {
6666
renderer.router = this;
6767

6868
this.enabled = true;
69+
this.initialized = false;
6970

7071
// make it possible to reset focus
7172
document.body.setAttribute('tabindex', '-1');
@@ -257,6 +258,8 @@ export class Router {
257258
);
258259
}
259260
});
261+
262+
this.initialized = true;
260263
}
261264

262265
#update_scroll_positions() {
@@ -283,7 +286,8 @@ export class Router {
283286
id: url.pathname + url.search,
284287
routes: this.routes.filter(([pattern]) => pattern.test(path)),
285288
url,
286-
path
289+
path,
290+
initial: !this.initialized
287291
};
288292
}
289293
}
@@ -333,7 +337,7 @@ export class Router {
333337

334338
/**
335339
* @param {URL} url
336-
* @returns {Promise<import('./types').NavigationResult>}
340+
* @returns {Promise<import('./types').NavigationResult | undefined>}
337341
*/
338342
async prefetch(url) {
339343
const info = this.parse(url);

packages/kit/src/runtime/client/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type NavigationInfo = {
55
routes: CSRRoute[];
66
url: URL;
77
path: string;
8+
initial: boolean;
89
};
910

1011
export type NavigationCandidate = {

packages/kit/test/apps/basics/src/routes/routing/index.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
<a href="/routing/a">a</a>
88
<a href="/routing/ambiguous/ok.json" rel="external">ok</a>
99
<a href="http://localhost:{$page.url.searchParams.get('port')}">elsewhere</a>
10+
<a href="/static.json">static.json</a>
1011

1112
<div class="hydrate-test" />

packages/kit/test/apps/basics/test/test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,6 +2112,15 @@ test.describe.parallel('Routing', () => {
21122112
await page.goto('/routing/rest/complex/prefix-one/two/three');
21132113
expect(await page.textContent('h1')).toBe('parts: one/two/three');
21142114
});
2115+
2116+
test('links to unmatched routes result in a full page navigation, not a 404', async ({
2117+
page,
2118+
clicknav
2119+
}) => {
2120+
await page.goto('/routing');
2121+
await clicknav('[href="/static.json"]');
2122+
expect(await page.textContent('body')).toBe('"static file"\n');
2123+
});
21152124
});
21162125

21172126
test.describe.parallel('Session', () => {

0 commit comments

Comments
 (0)