Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/red-jokes-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: add `getRequestEvent` to `$app/server`
68 changes: 68 additions & 0 deletions documentation/docs/20-core-concepts/20-load.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions packages/kit/src/runtime/app/server/event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/** @import { RequestEvent } from '@sveltejs/kit' */

/** @type {RequestEvent | null} */
let request_event = null;

/** @type {import('node:async_hooks').AsyncLocalStorage<RequestEvent | null>} */
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do users see on StackBlitz? Will it be the Can only read the current request event when the event is being processed message? That might be confusing if so and perhaps it's better to throw a different error here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

upgraded the message to include a mention of ALS, if it's absent

// 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 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();

if (!event) {
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;
}

/**
* @template T
* @param {RequestEvent | null} 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;
}
}
2 changes: 2 additions & 0 deletions packages/kit/src/runtime/app/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,5 @@ export function read(asset) {

throw new Error(`Asset does not exist: ${file}`);
}

export { getRequestEvent } from './event.js';
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -40,8 +41,8 @@ export async function render_endpoint(event, mod, state) {
}

try {
let response = await handler(
/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event)
let response = await with_event(event, () =>
handler(/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event))
);

if (!(response instanceof Response)) {
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/page/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -246,7 +247,7 @@ async function call_action(event, actions) {
);
}

return action(event);
return with_event(event, () => action(event));
}

/** @param {any} data */
Expand Down
151 changes: 81 additions & 70 deletions packages/kit/src/runtime/server/page/load_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -16,7 +17,6 @@ import { b64_encode } from '../../utils.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 = {
Expand All @@ -28,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,
() => {
Expand Down Expand Up @@ -58,92 +65,96 @@ 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);
let done = false;

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`
);
}
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);

// 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 && 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) {
validate_depends(node.server_id || 'missing route ID', dep);
// 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 (done && !uses.dependencies.has(href)) {
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 \`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}: Accessing \`params.${String(
key
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes`
);
}
}

uses.dependencies.add(href);
}
},
params: new Proxy(event.params, {
get: (target, key) => {
if (DEV && done && typeof key === 'string' && !uses.params.has(key)) {
if (is_tracking) {
uses.params.add(key);
}
return target[/** @type {string} */ (key)];
}
}),
parent: async () => {
if (DEV && done && !uses.parent) {
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 \`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.params.add(key);
uses.parent = true;
}
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`
);
}
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.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)];
}

if (is_tracking) {
uses.route = true;
}),
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);
Expand All @@ -155,7 +166,7 @@ export async function load_server_data({ event, state, node, parent }) {
type: 'data',
data: result ?? null,
uses,
slash: node.server.trailingSlash
slash
};
}

Expand Down
Loading
Loading