Skip to content

Commit 78a794f

Browse files
authored
feat: provide fetch to reroute (#13549)
This provides `fetch` as an argument to `reroute`, so that you can get the same benefits as the `fetch` provided to `load` functions from it. This is important (among other things) because else you might not be able to forward cookies on the server which influence where to go to.
1 parent adb5e25 commit 78a794f

File tree

12 files changed

+180
-117
lines changed

12 files changed

+180
-117
lines changed

.changeset/thirty-days-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: provide `fetch` to `reroute`

documentation/docs/30-advanced/20-hooks.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,26 @@ The `lang` parameter will be correctly derived from the returned pathname.
299299

300300
Using `reroute` will _not_ change the contents of the browser's address bar, or the value of `event.url`.
301301

302-
Since version 2.18, the `reroute` hook can be asynchronous, allowing it to (for example) fetch data from your backend to decide where to reroute to. Use this carefully and make sure it's fast, as it will delay navigation otherwise.
302+
Since version 2.18, the `reroute` hook can be asynchronous, allowing it to (for example) fetch data from your backend to decide where to reroute to. Use this carefully and make sure it's fast, as it will delay navigation otherwise. In case you fetch data, make sure to use the `fetch` provided to the `reroute` function. It has the [same benefits](load#Making-fetch-requests) as using the special `fetch` of `load` functions, with the caveat that `params` and `id` are unavailable to [`handleFetch`](#Server-hooks-handleFetch) because the route is not known yet.
303+
304+
```js
305+
/// file: src/hooks.js
306+
// @errors: 2345
307+
// @errors: 2304
308+
309+
/** @type {import('@sveltejs/kit').Reroute} */
310+
export function reroute({ url, fetch }) {
311+
// Ask a special endpoint within your app about the destination
312+
if (url.pathname === '/api/reroute') return;
313+
314+
const api = new URL('/api/reroute', url);
315+
api.searchParams.set('pathname', url.pathname);
316+
317+
const result = await fetch(api).then(r => r.json());
318+
return result.pathname;
319+
}
320+
```
321+
303322

304323
> [!NOTE] `reroute` is considered a pure, idempotent function. As such, it must always return the same output for the same input and not have side effects. Under these assumptions, SvelteKit caches the result of `reroute` on the client so it is only called once per unique URL.
305324

packages/kit/src/exports/public.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,7 @@ export type ClientInit = () => MaybePromise<void>;
814814
* The [`reroute`](https://svelte.dev/docs/kit/hooks#Universal-hooks-reroute) hook allows you to modify the URL before it is used to determine which route to render.
815815
* @since 2.3.0
816816
*/
817-
export type Reroute = (event: { url: URL }) => MaybePromise<void | string>;
817+
export type Reroute = (event: { url: URL; fetch: typeof fetch }) => MaybePromise<void | string>;
818818

819819
/**
820820
* The [`transport`](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) hook allows you to transport custom types across the server/client boundary.

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

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -690,12 +690,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node
690690
app.hash
691691
),
692692
async fetch(resource, init) {
693-
/** @type {URL | string} */
694-
let requested;
695-
696693
if (resource instanceof Request) {
697-
requested = resource.url;
698-
699694
// we're not allowed to modify the received `Request` object, so in order
700695
// to fixup relative urls we create a new equivalent `init` object instead
701696
init = {
@@ -720,25 +715,15 @@ async function load_node({ loader, parent, url, params, route, server_data_node
720715
signal: resource.signal,
721716
...init
722717
};
723-
} else {
724-
requested = resource;
725718
}
726719

727-
// we must fixup relative urls so they are resolved from the target page
728-
const resolved = new URL(requested, url);
720+
const { resolved, promise } = resolve_fetch_url(resource, init, url);
721+
729722
if (is_tracking) {
730723
depends(resolved.href);
731724
}
732725

733-
// match ssr serialized data url, which is important to find cached responses
734-
if (resolved.origin === url.origin) {
735-
requested = resolved.href.slice(url.origin.length);
736-
}
737-
738-
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
739-
return started
740-
? subsequent_fetch(requested, resolved.href, init)
741-
: initial_fetch(requested, init);
726+
return promise;
742727
},
743728
setHeaders: () => {}, // noop
744729
depends,
@@ -793,6 +778,30 @@ async function load_node({ loader, parent, url, params, route, server_data_node
793778
};
794779
}
795780

781+
/**
782+
* @param {Request | string | URL} input
783+
* @param {RequestInit | undefined} init
784+
* @param {URL} url
785+
*/
786+
function resolve_fetch_url(input, init, url) {
787+
let requested = input instanceof Request ? input.url : input;
788+
789+
// we must fixup relative urls so they are resolved from the target page
790+
const resolved = new URL(requested, url);
791+
792+
// match ssr serialized data url, which is important to find cached responses
793+
if (resolved.origin === url.origin) {
794+
requested = resolved.href.slice(url.origin.length);
795+
}
796+
797+
// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be resolved
798+
const promise = started
799+
? subsequent_fetch(requested, resolved.href, init)
800+
: initial_fetch(requested, init);
801+
802+
return { resolved, promise };
803+
}
804+
796805
/**
797806
* @param {boolean} parent_changed
798807
* @param {boolean} route_changed
@@ -1223,7 +1232,13 @@ async function get_rerouted_url(url) {
12231232
try {
12241233
const promise = (async () => {
12251234
// reroute could alter the given URL, so we pass a copy
1226-
let rerouted = (await app.hooks.reroute({ url: new URL(url) })) ?? url;
1235+
let rerouted =
1236+
(await app.hooks.reroute({
1237+
url: new URL(url),
1238+
fetch: async (input, init) => {
1239+
return resolve_fetch_url(input, init, url).promise;
1240+
}
1241+
})) ?? url;
12271242

12281243
if (typeof rerouted === 'string') {
12291244
const tmp = new URL(url); // do not mutate the incoming URL

packages/kit/src/runtime/server/cookie.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ function validate_options(options) {
2929
/**
3030
* @param {Request} request
3131
* @param {URL} url
32-
* @param {import('types').TrailingSlash} trailing_slash
3332
*/
34-
export function get_cookies(request, url, trailing_slash) {
33+
export function get_cookies(request, url) {
3534
const header = request.headers.get('cookie') ?? '';
3635
const initial_cookies = parse(header, { decode: (value) => value });
3736

38-
const normalized_url = normalize_path(url.pathname, trailing_slash);
37+
/** @type {string | undefined} */
38+
let normalized_url;
3939

4040
/** @type {Record<string, import('./page/types.js').Cookie>} */
4141
const new_cookies = {};
@@ -149,6 +149,9 @@ export function get_cookies(request, url, trailing_slash) {
149149
let path = options.path;
150150

151151
if (!options.domain || options.domain === url.hostname) {
152+
if (!normalized_url) {
153+
throw new Error('Cannot serialize cookies until after the route is determined');
154+
}
152155
path = resolve(normalized_url, path);
153156
}
154157

@@ -190,12 +193,20 @@ export function get_cookies(request, url, trailing_slash) {
190193
.join('; ');
191194
}
192195

196+
/** @type {Array<() => void>} */
197+
const internal_queue = [];
198+
193199
/**
194200
* @param {string} name
195201
* @param {string} value
196202
* @param {import('./page/types.js').Cookie['options']} options
197203
*/
198204
function set_internal(name, value, options) {
205+
if (!normalized_url) {
206+
internal_queue.push(() => set_internal(name, value, options));
207+
return;
208+
}
209+
199210
let path = options.path;
200211

201212
if (!options.domain || options.domain === url.hostname) {
@@ -220,7 +231,15 @@ export function get_cookies(request, url, trailing_slash) {
220231
}
221232
}
222233

223-
return { cookies, new_cookies, get_cookie_header, set_internal };
234+
/**
235+
* @param {import('types').TrailingSlash} trailing_slash
236+
*/
237+
function set_trailing_slash(trailing_slash) {
238+
normalized_url = normalize_path(url.pathname, trailing_slash);
239+
internal_queue.forEach((fn) => fn());
240+
}
241+
242+
return { cookies, new_cookies, get_cookie_header, set_internal, set_trailing_slash };
224243
}
225244

226245
/**

packages/kit/src/runtime/server/cookie.spec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ const cookies_setup = ({ href, headers } = {}) => {
5353
...headers
5454
})
5555
});
56-
return get_cookies(request, url, 'ignore');
56+
const result = get_cookies(request, url);
57+
result.set_trailing_slash('ignore');
58+
return result;
5759
};
5860

5961
test('a cookie should not be present after it is deleted', () => {

0 commit comments

Comments
 (0)