From 36698ae2940fd9cdb07a9c753fb0d1ab5647e36f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 10:47:38 -0400 Subject: [PATCH 01/12] feat: add `getRequestEvent` to `$app/server` --- .changeset/red-jokes-ring.md | 5 + packages/kit/src/runtime/app/server/event.js | 46 +++++ packages/kit/src/runtime/app/server/index.js | 2 + .../kit/src/runtime/server/page/load_data.js | 159 +++++++++--------- packages/kit/src/runtime/server/respond.js | 37 ++-- packages/kit/test/apps/basics/src/app.d.ts | 1 + .../kit/test/apps/basics/src/hooks.server.js | 14 ++ .../src/routes/get-request-event/+page.svelte | 1 + .../with-message/+page.server.ts | 26 +++ .../with-message/+page.svelte | 10 ++ packages/kit/test/apps/basics/test/test.js | 14 ++ packages/kit/types/index.d.ts | 8 + .../src/routes/request-event/+page.server.ts | 11 ++ .../src/routes/request-event/+page.svelte | 5 + 14 files changed, 246 insertions(+), 93 deletions(-) create mode 100644 .changeset/red-jokes-ring.md create mode 100644 packages/kit/src/runtime/app/server/event.js create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/+page.svelte create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.server.ts create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.svelte create mode 100644 playgrounds/basic/src/routes/request-event/+page.server.ts create mode 100644 playgrounds/basic/src/routes/request-event/+page.svelte diff --git a/.changeset/red-jokes-ring.md b/.changeset/red-jokes-ring.md new file mode 100644 index 000000000000..6c04b45ee0dd --- /dev/null +++ b/.changeset/red-jokes-ring.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: add `getRequestEvent` to `$app/server` diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js new file mode 100644 index 000000000000..f574452f5bab --- /dev/null +++ b/packages/kit/src/runtime/app/server/event.js @@ -0,0 +1,46 @@ +/** @import { RequestEvent } from '@sveltejs/kit' */ + +/** @type {RequestEvent | null} */ +let request_event = null; + +/** @type {import('node:async_hooks').AsyncLocalStorage} */ +let als; + +try { + const hooks = await import('node:async_hooks'); + als = new hooks.AsyncLocalStorage(); +} catch { + // can't use AsyncLocalStorage, but can still call getRequestEvent synchronously. + // this isn't behind `supports` because it's basically just StackBlitz (i.e. + // in-browser usage) that doesn't support it AFAICT +} + +/** + * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions + * called by them). + * + * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + */ +export function getRequestEvent() { + const event = request_event ?? als?.getStore(); + + if (!event) { + throw new Error('Can only read the current request event when the event is being processed'); + } + + return event; +} + +/** + * @template T + * @param {RequestEvent} event + * @param {() => T} fn + */ +export function with_event(event, fn) { + try { + request_event = event; + return als ? als.run(event, fn) : fn(); + } finally { + request_event = null; + } +} diff --git a/packages/kit/src/runtime/app/server/index.js b/packages/kit/src/runtime/app/server/index.js index 33c9b0a0d1ba..19c384932107 100644 --- a/packages/kit/src/runtime/app/server/index.js +++ b/packages/kit/src/runtime/app/server/index.js @@ -71,3 +71,5 @@ export function read(asset) { throw new Error(`Asset does not exist: ${file}`); } + +export { getRequestEvent } from './event.js'; diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 613093412137..846320edb610 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -2,6 +2,7 @@ import { DEV } from 'esm-env'; import { disable_search, make_trackable } from '../../../utils/url.js'; import { validate_depends } from '../../shared.js'; import { b64_encode } from '../../utils.js'; +import { with_event } from '../../app/server/event.js'; /** * Calls the user's server `load` function. @@ -58,92 +59,98 @@ export async function load_server_data({ event, state, node, parent }) { disable_search(url); } - const result = await node.server.load?.call(null, { - ...event, - fetch: (info, init) => { - const url = new URL(info instanceof Request ? info.url : info, event.url); + const load = node.server.load; - if (DEV && done && !uses.dependencies.has(url.href)) { - console.warn( - `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` - ); - } - - // Note: server fetches are not added to uses.depends due to security concerns - return event.fetch(info, init); - }, - /** @param {string[]} deps */ - depends: (...deps) => { - for (const dep of deps) { - const { href } = new URL(dep, event.url); + const result = + load && + (await with_event(event, () => + load.call(null, { + ...event, + fetch: (info, init) => { + const url = new URL(info instanceof Request ? info.url : info, event.url); - if (DEV) { - validate_depends(node.server_id || 'missing route ID', dep); - - if (done && !uses.dependencies.has(href)) { + if (DEV && done && !uses.dependencies.has(url.href)) { console.warn( - `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` ); } - } - uses.dependencies.add(href); - } - }, - params: new Proxy(event.params, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { - console.warn( - `${node.server_id}: Accessing \`params.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` - ); - } + // Note: server fetches are not added to uses.depends due to security concerns + return event.fetch(info, init); + }, + /** @param {string[]} deps */ + depends: (...deps) => { + for (const dep of deps) { + const { href } = new URL(dep, event.url); + + if (DEV) { + validate_depends(node.server_id || 'missing route ID', dep); + + if (done && !uses.dependencies.has(href)) { + console.warn( + `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + ); + } + } - if (is_tracking) { - uses.params.add(key); - } - return target[/** @type {string} */ (key)]; - } - }), - parent: async () => { - if (DEV && done && !uses.parent) { - console.warn( - `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` - ); - } + uses.dependencies.add(href); + } + }, + params: new Proxy(event.params, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { + console.warn( + `${node.server_id}: Accessing \`params.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` + ); + } - if (is_tracking) { - uses.parent = true; - } - return parent(); - }, - route: new Proxy(event.route, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.route) { - console.warn( - `${node.server_id}: Accessing \`route.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` - ); - } + if (is_tracking) { + uses.params.add(key); + } + return target[/** @type {string} */ (key)]; + } + }), + parent: async () => { + if (DEV && done && !uses.parent) { + console.warn( + `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` + ); + } - if (is_tracking) { - uses.route = true; + if (is_tracking) { + uses.parent = true; + } + return parent(); + }, + route: new Proxy(event.route, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.route) { + console.warn( + `${node.server_id}: Accessing \`route.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` + ); + } + + if (is_tracking) { + uses.route = true; + } + return target[/** @type {'id'} */ (key)]; + } + }), + url, + untrack(fn) { + is_tracking = false; + try { + return fn(); + } finally { + is_tracking = true; + } } - return target[/** @type {'id'} */ (key)]; - } - }), - url, - untrack(fn) { - is_tracking = false; - try { - return fn(); - } finally { - is_tracking = true; - } - } - }); + }) + )); if (__SVELTEKIT_DEV__) { validate_load_response(result, node.server_id); diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 38ed27302b0d..b943691233d0 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -33,6 +33,7 @@ import { strip_data_suffix, strip_resolution_suffix } from '../pathname.js'; +import { with_event } from '../app/server/event.js'; /* global __SVELTEKIT_ADAPTER_NAME__ */ /* global __SVELTEKIT_DEV__ */ @@ -350,26 +351,28 @@ export async function respond(request, options, manifest, state) { if (state.prerendering && !state.prerendering.fallback) disable_search(url); - const response = await options.hooks.handle({ - event, - resolve: (event, opts) => - resolve(event, page_nodes, opts).then((response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } + const response = await with_event(event, () => + options.hooks.handle({ + event, + resolve: (event, opts) => + resolve(event, page_nodes, opts).then((response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } - add_cookies_to_headers(response.headers, Object.values(new_cookies)); + add_cookies_to_headers(response.headers, Object.values(new_cookies)); - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); - } + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } - return response; - }) - }); + return response; + }) + }) + ); // respond with 304 if etag matches if (response.status === 200 && response.headers.has('etag')) { diff --git a/packages/kit/test/apps/basics/src/app.d.ts b/packages/kit/test/apps/basics/src/app.d.ts index 16bdf501b907..f40b6d8c868a 100644 --- a/packages/kit/test/apps/basics/src/app.d.ts +++ b/packages/kit/test/apps/basics/src/app.d.ts @@ -6,6 +6,7 @@ declare global { key: string; params: Record; url?: URL; + message?: string; } interface PageState { diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index 1c825a6a6c90..1caadcf7db5b 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -4,6 +4,7 @@ import { sequence } from '@sveltejs/kit/hooks'; import fs from 'node:fs'; import { COOKIE_NAME } from './routes/cookies/shared'; import { _set_from_init } from './routes/init-hooks/+page.server'; +import { getRequestEvent } from '$app/server'; /** * Transform an error into a POJO, by copying its `name`, `message` @@ -150,6 +151,19 @@ export const handle = sequence( if (['/non-existent-route', '/non-existent-route-loop'].includes(event.url.pathname)) { event.locals.url = new URL(event.request.url); } + return resolve(event); + }, + async ({ event, resolve }) => { + if (event.url.pathname === '/get-request-event/with-message') { + const e = getRequestEvent(); + + if (event !== e) { + throw new Error('event !== e'); + } + + e.locals.message = 'hello from hooks.server.js'; + } + return resolve(event); } ); diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/+page.svelte b/packages/kit/test/apps/basics/src/routes/get-request-event/+page.svelte new file mode 100644 index 000000000000..2a7fa697c765 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/+page.svelte @@ -0,0 +1 @@ +go diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.server.ts b/packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.server.ts new file mode 100644 index 000000000000..b0c59da7ab36 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.server.ts @@ -0,0 +1,26 @@ +import { getRequestEvent } from '$app/server'; + +export async function load() { + const e1 = getRequestEvent(); + await Promise.resolve(); + const e2 = getRequestEvent(); // check AsyncLocalStorage works + + if (e1 !== e2) { + throw new Error('e1 !== e2'); + } + + return { + message: e1.locals.message + }; +} + +export const actions = { + default: async () => { + const { request } = getRequestEvent(); + const data = await request.formData(); + + return { + message: `from form: ${data.get('message')}` + }; + } +}; diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.svelte b/packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.svelte new file mode 100644 index 000000000000..9a05621baa38 --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/with-message/+page.svelte @@ -0,0 +1,10 @@ + + +

{form?.message ?? data.message}

+ +
+ + +
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js index c5867f34e00c..52d6a5bf8d78 100644 --- a/packages/kit/test/apps/basics/test/test.js +++ b/packages/kit/test/apps/basics/test/test.js @@ -1532,3 +1532,17 @@ test.describe('Serialization', () => { expect(await page.textContent('h1')).toBe('It works!'); }); }); + +test.describe('getRequestEvent', () => { + test('getRequestEvent works in hooks, load functions and actions', async ({ page, clicknav }) => { + await page.goto('/get-request-event'); + await clicknav('[href="/get-request-event/with-message"]'); + + expect(await page.textContent('h1')).toBe('hello from hooks.server.js'); + + await page.locator('input[name="message"]').fill('hello'); + await page.click('button'); + + expect(await page.textContent('h1')).toBe('from form: hello'); + }); +}); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index af14891d66d3..21eaf59e4837 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2397,6 +2397,7 @@ declare module '$app/paths' { } declare module '$app/server' { + import type { RequestEvent } from '@sveltejs/kit'; /** * Read the contents of an imported asset from the filesystem * @example @@ -2410,6 +2411,13 @@ declare module '$app/server' { * @since 2.4.0 */ export function read(asset: string): Response; + /** + * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions + * called by them). + * + * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + */ + export function getRequestEvent(): RequestEvent>, string | null>; export {}; } diff --git a/playgrounds/basic/src/routes/request-event/+page.server.ts b/playgrounds/basic/src/routes/request-event/+page.server.ts new file mode 100644 index 000000000000..e81bd63fc51e --- /dev/null +++ b/playgrounds/basic/src/routes/request-event/+page.server.ts @@ -0,0 +1,11 @@ +import { getRequestEvent } from '$app/server'; + +export async function load() { + await Promise.resolve(); + + const event = getRequestEvent(); + + return { + pathname: event.url.pathname + }; +} diff --git a/playgrounds/basic/src/routes/request-event/+page.svelte b/playgrounds/basic/src/routes/request-event/+page.svelte new file mode 100644 index 000000000000..8e3cefb98577 --- /dev/null +++ b/playgrounds/basic/src/routes/request-event/+page.svelte @@ -0,0 +1,5 @@ + + +

{data.pathname}

From 66b8190e4756727e91aabad4231e8876f04409ad Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 14:02:09 -0400 Subject: [PATCH 02/12] reduce indentation --- .../kit/src/runtime/server/page/load_data.js | 156 +++++++++--------- 1 file changed, 80 insertions(+), 76 deletions(-) diff --git a/packages/kit/src/runtime/server/page/load_data.js b/packages/kit/src/runtime/server/page/load_data.js index 846320edb610..2ada2b6ecde6 100644 --- a/packages/kit/src/runtime/server/page/load_data.js +++ b/packages/kit/src/runtime/server/page/load_data.js @@ -17,7 +17,6 @@ import { with_event } from '../../app/server/event.js'; export async function load_server_data({ event, state, node, parent }) { if (!node?.server) return null; - let done = false; let is_tracking = true; const uses = { @@ -29,6 +28,13 @@ export async function load_server_data({ event, state, node, parent }) { search_params: new Set() }; + const load = node.server.load; + const slash = node.server.trailingSlash; + + if (!load) { + return { type: 'data', data: null, uses, slash }; + } + const url = make_trackable( event.url, () => { @@ -59,98 +65,96 @@ export async function load_server_data({ event, state, node, parent }) { disable_search(url); } - const load = node.server.load; + let done = false; - const result = - load && - (await with_event(event, () => - load.call(null, { - ...event, - fetch: (info, init) => { - const url = new URL(info instanceof Request ? info.url : info, event.url); + const result = await with_event(event, () => + load.call(null, { + ...event, + fetch: (info, init) => { + const url = new URL(info instanceof Request ? info.url : info, event.url); - if (DEV && done && !uses.dependencies.has(url.href)) { - console.warn( - `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` - ); - } + if (DEV && done && !uses.dependencies.has(url.href)) { + console.warn( + `${node.server_id}: Calling \`event.fetch(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` + ); + } - // Note: server fetches are not added to uses.depends due to security concerns - return event.fetch(info, init); - }, - /** @param {string[]} deps */ - depends: (...deps) => { - for (const dep of deps) { - const { href } = new URL(dep, event.url); - - if (DEV) { - validate_depends(node.server_id || 'missing route ID', dep); - - if (done && !uses.dependencies.has(href)) { - console.warn( - `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` - ); - } - } + // Note: server fetches are not added to uses.depends due to security concerns + return event.fetch(info, init); + }, + /** @param {string[]} deps */ + depends: (...deps) => { + for (const dep of deps) { + const { href } = new URL(dep, event.url); - uses.dependencies.add(href); - } - }, - params: new Proxy(event.params, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { + if (DEV) { + validate_depends(node.server_id || 'missing route ID', dep); + + if (done && !uses.dependencies.has(href)) { console.warn( - `${node.server_id}: Accessing \`params.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` + `${node.server_id}: Calling \`depends(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the dependency is invalidated` ); } - - if (is_tracking) { - uses.params.add(key); - } - return target[/** @type {string} */ (key)]; } - }), - parent: async () => { - if (DEV && done && !uses.parent) { + + uses.dependencies.add(href); + } + }, + params: new Proxy(event.params, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.params.has(key)) { console.warn( - `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` + `${node.server_id}: Accessing \`params.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes` ); } if (is_tracking) { - uses.parent = true; + uses.params.add(key); } - return parent(); - }, - route: new Proxy(event.route, { - get: (target, key) => { - if (DEV && done && typeof key === 'string' && !uses.route) { - console.warn( - `${node.server_id}: Accessing \`route.${String( - key - )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` - ); - } + return target[/** @type {string} */ (key)]; + } + }), + parent: async () => { + if (DEV && done && !uses.parent) { + console.warn( + `${node.server_id}: Calling \`parent(...)\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when parent data changes` + ); + } - if (is_tracking) { - uses.route = true; - } - return target[/** @type {'id'} */ (key)]; + if (is_tracking) { + uses.parent = true; + } + return parent(); + }, + route: new Proxy(event.route, { + get: (target, key) => { + if (DEV && done && typeof key === 'string' && !uses.route) { + console.warn( + `${node.server_id}: Accessing \`route.${String( + key + )}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes` + ); } - }), - url, - untrack(fn) { - is_tracking = false; - try { - return fn(); - } finally { - is_tracking = true; + + if (is_tracking) { + uses.route = true; } + return target[/** @type {'id'} */ (key)]; + } + }), + url, + untrack(fn) { + is_tracking = false; + try { + return fn(); + } finally { + is_tracking = true; } - }) - )); + } + }) + ); if (__SVELTEKIT_DEV__) { validate_load_response(result, node.server_id); @@ -162,7 +166,7 @@ export async function load_server_data({ event, state, node, parent }) { type: 'data', data: result ?? null, uses, - slash: node.server.trailingSlash + slash }; } From b37f331fbabc61a4ad3a40163f4046ba89816711 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 14:10:18 -0400 Subject: [PATCH 03/12] innocuous change to try and trigger a docs preview --- packages/kit/src/runtime/app/server/event.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index f574452f5bab..689882357735 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -16,8 +16,7 @@ try { } /** - * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions - * called by them). + * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ From 931ab212600209deb8dd9a257c68b8b28c24cf47 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 14:12:25 -0400 Subject: [PATCH 04/12] regenerate --- packages/kit/types/index.d.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 21eaf59e4837..ff6d380ed7e6 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2412,8 +2412,7 @@ declare module '$app/server' { */ export function read(asset: string): Response; /** - * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions - * called by them). + * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ From f2053c11ca3a775720e532545ce7aab2dd65786c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 14:30:12 -0400 Subject: [PATCH 05/12] more detailed error message --- packages/kit/src/runtime/app/server/event.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index 689882357735..800ac1f02fbd 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -24,7 +24,15 @@ export function getRequestEvent() { const event = request_event ?? als?.getStore(); if (!event) { - throw new Error('Can only read the current request event when the event is being processed'); + let message = + 'Can only read the current request event inside functions invoked during `handle`, such as server `load` functions, actions, and server endpoints.'; + + if (!als) { + message += + ' In environments without `AsyncLocalStorage`, the event must be read synchronously, not after an `await`.'; + } + + throw new Error(message); } return event; From 6fc3941529b830372b1f84f563e745e0b1767df7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 15:02:14 -0400 Subject: [PATCH 06/12] tighten up --- packages/kit/src/runtime/app/server/event.js | 4 +-- packages/kit/src/runtime/server/endpoint.js | 5 +-- .../kit/src/runtime/server/page/actions.js | 3 +- packages/kit/src/runtime/server/respond.js | 36 ++++++++++--------- .../kit/test/apps/basics/src/hooks.server.js | 2 +- .../get-request-event/endpoint/+server.js | 10 ++++++ .../kit/test/apps/basics/test/server.test.js | 7 ++++ 7 files changed, 45 insertions(+), 22 deletions(-) create mode 100644 packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index 800ac1f02fbd..d6a261da425d 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -3,7 +3,7 @@ /** @type {RequestEvent | null} */ let request_event = null; -/** @type {import('node:async_hooks').AsyncLocalStorage} */ +/** @type {import('node:async_hooks').AsyncLocalStorage} */ let als; try { @@ -40,7 +40,7 @@ export function getRequestEvent() { /** * @template T - * @param {RequestEvent} event + * @param {RequestEvent | null} event * @param {() => T} fn */ export function with_event(event, fn) { diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js index 55bcd87807b9..417fa8e9fb95 100644 --- a/packages/kit/src/runtime/server/endpoint.js +++ b/packages/kit/src/runtime/server/endpoint.js @@ -1,5 +1,6 @@ import { ENDPOINT_METHODS, PAGE_METHODS } from '../../constants.js'; import { negotiate } from '../../utils/http.js'; +import { with_event } from '../app/server/event.js'; import { Redirect } from '../control.js'; import { method_not_allowed } from './utils.js'; @@ -40,8 +41,8 @@ export async function render_endpoint(event, mod, state) { } try { - let response = await handler( - /** @type {import('@sveltejs/kit').RequestEvent>} */ (event) + let response = await with_event(event, () => + handler(/** @type {import('@sveltejs/kit').RequestEvent>} */ (event)) ); if (!(response instanceof Response)) { diff --git a/packages/kit/src/runtime/server/page/actions.js b/packages/kit/src/runtime/server/page/actions.js index 80879e62e952..e7703359234e 100644 --- a/packages/kit/src/runtime/server/page/actions.js +++ b/packages/kit/src/runtime/server/page/actions.js @@ -5,6 +5,7 @@ import { get_status, normalize_error } from '../../../utils/error.js'; import { is_form_content_type, negotiate } from '../../../utils/http.js'; import { HttpError, Redirect, ActionFailure, SvelteKitError } from '../../control.js'; import { handle_error_and_jsonify } from '../utils.js'; +import { with_event } from '../../app/server/event.js'; /** @param {import('@sveltejs/kit').RequestEvent} event */ export function is_action_json_request(event) { @@ -246,7 +247,7 @@ async function call_action(event, actions) { ); } - return action(event); + return with_event(event, () => action(event)); } /** @param {any} data */ diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index b943691233d0..812ef3df7a69 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -355,22 +355,26 @@ export async function respond(request, options, manifest, state) { options.hooks.handle({ event, resolve: (event, opts) => - resolve(event, page_nodes, opts).then((response) => { - // add headers/cookies here, rather than inside `resolve`, so that we - // can do it once for all responses instead of once per `return` - for (const key in headers) { - const value = headers[key]; - response.headers.set(key, /** @type {string} */ (value)); - } - - add_cookies_to_headers(response.headers, Object.values(new_cookies)); - - if (state.prerendering && event.route.id !== null) { - response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); - } - - return response; - }) + // counter-intuitively, we need to clear the event, so that it's not + // e.g. accessible when loading modules needed to handle the request + with_event(null, () => + resolve(event, page_nodes, opts).then((response) => { + // add headers/cookies here, rather than inside `resolve`, so that we + // can do it once for all responses instead of once per `return` + for (const key in headers) { + const value = headers[key]; + response.headers.set(key, /** @type {string} */ (value)); + } + + add_cookies_to_headers(response.headers, Object.values(new_cookies)); + + if (state.prerendering && event.route.id !== null) { + response.headers.set('x-sveltekit-routeid', encodeURI(event.route.id)); + } + + return response; + }) + ) }) ); diff --git a/packages/kit/test/apps/basics/src/hooks.server.js b/packages/kit/test/apps/basics/src/hooks.server.js index 1caadcf7db5b..a9aa2bc0ceb9 100644 --- a/packages/kit/test/apps/basics/src/hooks.server.js +++ b/packages/kit/test/apps/basics/src/hooks.server.js @@ -154,7 +154,7 @@ export const handle = sequence( return resolve(event); }, async ({ event, resolve }) => { - if (event.url.pathname === '/get-request-event/with-message') { + if (event.url.pathname.startsWith('/get-request-event/')) { const e = getRequestEvent(); if (event !== e) { diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js b/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js new file mode 100644 index 000000000000..88a7e89da9bd --- /dev/null +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js @@ -0,0 +1,10 @@ +import { getRequestEvent } from '$app/server'; +import { text } from '@sveltejs/kit'; + +export function GET() { + const event = getRequestEvent(); + + console.log(event.locals); + + return text(event.locals.message); +} diff --git a/packages/kit/test/apps/basics/test/server.test.js b/packages/kit/test/apps/basics/test/server.test.js index 0135b617ff94..905c345a9681 100644 --- a/packages/kit/test/apps/basics/test/server.test.js +++ b/packages/kit/test/apps/basics/test/server.test.js @@ -702,3 +702,10 @@ test.describe('init', () => { await expect(page.locator('p')).toHaveText('1'); }); }); + +test.describe('getRequestEvent', () => { + test('getRequestEvent works in server endpoints', async ({ request }) => { + const response = await request.get('/get-request-event/endpoint'); + expect(await response.text()).toBe('hello from hooks.server.js'); + }); +}); From ede1a56a315bd8a789d66d3c997bfb020e4c8d38 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 15:21:31 -0400 Subject: [PATCH 07/12] innocuous change to try and trigger a docs preview --- packages/kit/src/runtime/app/server/event.js | 2 +- packages/kit/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index d6a261da425d..957aff363e1c 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -18,7 +18,7 @@ try { /** * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * - * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ export function getRequestEvent() { const event = request_event ?? als?.getStore(); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index ff6d380ed7e6..b6d5def373af 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2414,7 +2414,7 @@ declare module '$app/server' { /** * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * - * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ export function getRequestEvent(): RequestEvent>, string | null>; From 0b85cb9acadcc2dfca39dd647bd9c8f9a2de5abd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 15:24:19 -0400 Subject: [PATCH 08/12] innocuous change to try and trigger a docs preview --- packages/kit/src/runtime/app/server/event.js | 2 +- packages/kit/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index 957aff363e1c..d6a261da425d 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -18,7 +18,7 @@ try { /** * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * - * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ export function getRequestEvent() { const event = request_event ?? als?.getStore(); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b6d5def373af..ff6d380ed7e6 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2414,7 +2414,7 @@ declare module '$app/server' { /** * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * - * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ export function getRequestEvent(): RequestEvent>, string | null>; From 7c541c18a0e914800456ac8f39c06cb014ca69ea Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 15:51:26 -0400 Subject: [PATCH 09/12] innocuous change to try and trigger a docs preview --- packages/kit/src/runtime/app/server/event.js | 2 +- packages/kit/types/index.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index d6a261da425d..957aff363e1c 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -18,7 +18,7 @@ try { /** * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * - * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ export function getRequestEvent() { const event = request_event ?? als?.getStore(); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index ff6d380ed7e6..b6d5def373af 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2414,7 +2414,7 @@ declare module '$app/server' { /** * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * - * In environments that do not support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). */ export function getRequestEvent(): RequestEvent>, string | null>; From 62648f59a13232146bc4e2ce5933683454ae72d2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 17:18:42 -0400 Subject: [PATCH 10/12] Update packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../basics/src/routes/get-request-event/endpoint/+server.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js b/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js index 88a7e89da9bd..c8136555431e 100644 --- a/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js +++ b/packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js @@ -4,7 +4,5 @@ import { text } from '@sveltejs/kit'; export function GET() { const event = getRequestEvent(); - console.log(event.locals); - return text(event.locals.message); } From 98dcd900be4db9ed76be497ddb09f21e8764b6b3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 17:37:10 -0400 Subject: [PATCH 11/12] add since tag --- packages/kit/src/runtime/app/server/event.js | 1 + packages/kit/types/index.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/kit/src/runtime/app/server/event.js b/packages/kit/src/runtime/app/server/event.js index 957aff363e1c..ca727da5d96e 100644 --- a/packages/kit/src/runtime/app/server/event.js +++ b/packages/kit/src/runtime/app/server/event.js @@ -19,6 +19,7 @@ try { * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * @since 2.20.0 */ export function getRequestEvent() { const event = request_event ?? als?.getStore(); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index b6d5def373af..0d9d0b4288e4 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2415,6 +2415,7 @@ declare module '$app/server' { * Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them). * * In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`). + * @since 2.20.0 */ export function getRequestEvent(): RequestEvent>, string | null>; From ddc23816184da5fec10d0c93d135ce08a6a69f98 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 15 Mar 2025 12:33:59 -0400 Subject: [PATCH 12/12] add some docs --- .../docs/20-core-concepts/20-load.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/documentation/docs/20-core-concepts/20-load.md b/documentation/docs/20-core-concepts/20-load.md index 933d38563875..b944b8b600fd 100644 --- a/documentation/docs/20-core-concepts/20-load.md +++ b/documentation/docs/20-core-concepts/20-load.md @@ -713,6 +713,74 @@ To prevent data waterfalls and preserve layout `load` caches: Putting an auth guard in `+layout.server.js` requires all child pages to call `await parent()` before protected code. Unless every child page depends on returned data from `await parent()`, the other options will be more performant. +## Using `getRequestEvent` + +When running server `load` functions, the `event` object passed to the function as an argument can also be retrieved with [`getRequestEvent`]($app-server#getRequestEvent). This allows shared logic (such as authentication guards) to access information about the current request without it needing to be passed around. + +For example, you might have a function that requires users to be logged in, and redirects them to `/login` if not: + +```js +/// file: src/lib/server/auth.js +// @filename: ambient.d.ts +interface User { + name: string; +} + +declare namespace App { + interface Locals { + user?: User; + } +} + +// @filename: index.ts +// ---cut--- +import { redirect } from '@sveltejs/kit'; +import { getRequestEvent } from '$app/server'; + +export function requireLogin() { + const { locals, url } = getRequestEvent(); + + // assume `locals.user` is populated in `handle` + if (!locals.user) { + const redirectTo = url.pathname + url.search; + const params = new URLSearchParams({ redirectTo }); + + redirect(307, `/login?${params}`); + } + + return locals.user; +} +``` + +Now, you can call `requireLogin` in any `load` function (or [form action](form-actions), for example) to guarantee that the user is logged in: + +```js +/// file: +page.server.js +// @filename: ambient.d.ts + +declare module '$lib/server/auth' { + interface User { + name: string; + } + + export function requireLogin(): User; +} + +// @filename: index.ts +// ---cut--- +import { requireLogin } from '$lib/server/auth'; + +export function load() { + const user = requireLogin(); + + // `user` is guaranteed to be a user object here, because otherwise + // `requireLogin` would throw a redirect and we wouldn't get here + return { + message: `hello ${user.name}!` + }; +} +``` + ## Further reading - [Tutorial: Loading data](/tutorial/kit/page-data)