diff --git a/.changeset/early-clocks-sneeze.md b/.changeset/early-clocks-sneeze.md new file mode 100644 index 000000000000..5c826e1d6b6e --- /dev/null +++ b/.changeset/early-clocks-sneeze.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +The redirect property returned from a module's load function must now be a properly encoded URI string value. diff --git a/documentation/docs/03-loading.md b/documentation/docs/03-loading.md index 8673c1f705a4..ae3a54c60ba0 100644 --- a/documentation/docs/03-loading.md +++ b/documentation/docs/03-loading.md @@ -143,6 +143,8 @@ If something goes wrong during `load`, return an `Error` object or a `string` de If the page should redirect (because the page is deprecated, or the user needs to be logged in, or whatever else) return a `string` containing the location to which they should be redirected alongside a `3xx` status code. +The `redirect` string should be a [properly encoded](https://en.wikipedia.org/wiki/Percent-encoding) URI. Both absolute and relative URIs are acceptable. + #### maxage To cause pages to be cached, return a `number` describing the page's max age in seconds. The resulting cache header will include `private` if user data was involved in rendering the page (either via `session`, or because a credentialed `fetch` was made in a `load` function), but otherwise will include `public` so that it can be cached by CDNs. diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index c38fb7264365..800d709177a1 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -117,7 +117,7 @@ export async function respond(opts) { { status: loaded.loaded.status, headers: { - location: encodeURI(loaded.loaded.redirect) + location: loaded.loaded.redirect } }, set_cookie_headers diff --git a/packages/kit/test/apps/basics/src/routes/encoded/index.svelte b/packages/kit/test/apps/basics/src/routes/encoded/index.svelte index 4db0033fc2d1..518e44c6a39b 100644 --- a/packages/kit/test/apps/basics/src/routes/encoded/index.svelte +++ b/packages/kit/test/apps/basics/src/routes/encoded/index.svelte @@ -1,3 +1,4 @@ 苗条 土豆 反应 +Redirect diff --git a/packages/kit/test/apps/basics/src/routes/encoded/redirect.svelte b/packages/kit/test/apps/basics/src/routes/encoded/redirect.svelte new file mode 100644 index 000000000000..1acc12eba077 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/encoded/redirect.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/kit/test/apps/basics/src/routes/encoded/redirected.svelte b/packages/kit/test/apps/basics/src/routes/encoded/redirected.svelte new file mode 100644 index 000000000000..7e8bc13d67c7 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/encoded/redirected.svelte @@ -0,0 +1,17 @@ + + + +
{embedded}diff --git "a/packages/kit/test/apps/basics/src/routes/encoded/\345\217\215\345\272\224.svelte" "b/packages/kit/test/apps/basics/src/routes/encoded/\345\217\215\345\272\224.svelte" index f25ac54f41fc..a31f8818f6f9 100644 --- "a/packages/kit/test/apps/basics/src/routes/encoded/\345\217\215\345\272\224.svelte" +++ "b/packages/kit/test/apps/basics/src/routes/encoded/\345\217\215\345\272\224.svelte" @@ -2,7 +2,7 @@ export function load() { return { status: 307, - redirect: '苗条' + redirect: encodeURI('苗条') }; } diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index 469ebe3d9eeb..21f49a5e04a0 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -472,6 +472,22 @@ test.describe.parallel('Encoded paths', () => { expect(decodeURI(await page.innerHTML('h3'))).toBe('/encoded/苗条'); }); + test('redirects do not re-encode the redirect string', async ({ page, clicknav }) => { + await page.goto('/encoded'); + + await clicknav('[href="/encoded/redirect"]'); + + // check innerText instead of innerHTML because innerHTML would return the '&' character reference instead of '&' character. + expect(await page.innerText('pre')).toBe('/苗条?foo=bar&fizz=buzz'); + }); + + test('redirects do not re-encode the redirect string during ssr', async ({ page }) => { + await page.goto('/encoded/redirect'); + + // check innerText instead of innerHTML because innerHTML would return the '&' character reference instead of '&' character. + expect(await page.innerText('pre')).toBe('/苗条?foo=bar&fizz=buzz'); + }); + test('sets charset on JSON Content-Type', async ({ request }) => { const response = await request.get('/encoded/endpoint'); expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');