Skip to content

Commit d1c0c68

Browse files
feat: add getRequestEvent to $app/server (#13582)
* feat: add `getRequestEvent` to `$app/server` * reduce indentation * innocuous change to try and trigger a docs preview * regenerate * more detailed error message * tighten up * innocuous change to try and trigger a docs preview * innocuous change to try and trigger a docs preview * innocuous change to try and trigger a docs preview * Update packages/kit/test/apps/basics/src/routes/get-request-event/endpoint/+server.js Co-authored-by: Simon H <[email protected]> * add since tag * add some docs --------- Co-authored-by: Simon H <[email protected]>
1 parent d9bb950 commit d1c0c68

File tree

19 files changed

+347
-93
lines changed

19 files changed

+347
-93
lines changed

.changeset/red-jokes-ring.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: add `getRequestEvent` to `$app/server`

documentation/docs/20-core-concepts/20-load.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,74 @@ To prevent data waterfalls and preserve layout `load` caches:
713713

714714
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.
715715

716+
## Using `getRequestEvent`
717+
718+
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.
719+
720+
For example, you might have a function that requires users to be logged in, and redirects them to `/login` if not:
721+
722+
```js
723+
/// file: src/lib/server/auth.js
724+
// @filename: ambient.d.ts
725+
interface User {
726+
name: string;
727+
}
728+
729+
declare namespace App {
730+
interface Locals {
731+
user?: User;
732+
}
733+
}
734+
735+
// @filename: index.ts
736+
// ---cut---
737+
import { redirect } from '@sveltejs/kit';
738+
import { getRequestEvent } from '$app/server';
739+
740+
export function requireLogin() {
741+
const { locals, url } = getRequestEvent();
742+
743+
// assume `locals.user` is populated in `handle`
744+
if (!locals.user) {
745+
const redirectTo = url.pathname + url.search;
746+
const params = new URLSearchParams({ redirectTo });
747+
748+
redirect(307, `/login?${params}`);
749+
}
750+
751+
return locals.user;
752+
}
753+
```
754+
755+
Now, you can call `requireLogin` in any `load` function (or [form action](form-actions), for example) to guarantee that the user is logged in:
756+
757+
```js
758+
/// file: +page.server.js
759+
// @filename: ambient.d.ts
760+
761+
declare module '$lib/server/auth' {
762+
interface User {
763+
name: string;
764+
}
765+
766+
export function requireLogin(): User;
767+
}
768+
769+
// @filename: index.ts
770+
// ---cut---
771+
import { requireLogin } from '$lib/server/auth';
772+
773+
export function load() {
774+
const user = requireLogin();
775+
776+
// `user` is guaranteed to be a user object here, because otherwise
777+
// `requireLogin` would throw a redirect and we wouldn't get here
778+
return {
779+
message: `hello ${user.name}!`
780+
};
781+
}
782+
```
783+
716784
## Further reading
717785
718786
- [Tutorial: Loading data](/tutorial/kit/page-data)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/** @import { RequestEvent } from '@sveltejs/kit' */
2+
3+
/** @type {RequestEvent | null} */
4+
let request_event = null;
5+
6+
/** @type {import('node:async_hooks').AsyncLocalStorage<RequestEvent | null>} */
7+
let als;
8+
9+
try {
10+
const hooks = await import('node:async_hooks');
11+
als = new hooks.AsyncLocalStorage();
12+
} catch {
13+
// can't use AsyncLocalStorage, but can still call getRequestEvent synchronously.
14+
// this isn't behind `supports` because it's basically just StackBlitz (i.e.
15+
// in-browser usage) that doesn't support it AFAICT
16+
}
17+
18+
/**
19+
* Returns the current `RequestEvent`. Can be used inside `handle`, `load` and actions (and functions called by them).
20+
*
21+
* In environments without [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage), this must be called synchronously (i.e. not after an `await`).
22+
* @since 2.20.0
23+
*/
24+
export function getRequestEvent() {
25+
const event = request_event ?? als?.getStore();
26+
27+
if (!event) {
28+
let message =
29+
'Can only read the current request event inside functions invoked during `handle`, such as server `load` functions, actions, and server endpoints.';
30+
31+
if (!als) {
32+
message +=
33+
' In environments without `AsyncLocalStorage`, the event must be read synchronously, not after an `await`.';
34+
}
35+
36+
throw new Error(message);
37+
}
38+
39+
return event;
40+
}
41+
42+
/**
43+
* @template T
44+
* @param {RequestEvent | null} event
45+
* @param {() => T} fn
46+
*/
47+
export function with_event(event, fn) {
48+
try {
49+
request_event = event;
50+
return als ? als.run(event, fn) : fn();
51+
} finally {
52+
request_event = null;
53+
}
54+
}

packages/kit/src/runtime/app/server/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,5 @@ export function read(asset) {
7171

7272
throw new Error(`Asset does not exist: ${file}`);
7373
}
74+
75+
export { getRequestEvent } from './event.js';

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ENDPOINT_METHODS, PAGE_METHODS } from '../../constants.js';
22
import { negotiate } from '../../utils/http.js';
3+
import { with_event } from '../app/server/event.js';
34
import { Redirect } from '../control.js';
45
import { method_not_allowed } from './utils.js';
56

@@ -40,8 +41,8 @@ export async function render_endpoint(event, mod, state) {
4041
}
4142

4243
try {
43-
let response = await handler(
44-
/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event)
44+
let response = await with_event(event, () =>
45+
handler(/** @type {import('@sveltejs/kit').RequestEvent<Record<string, any>>} */ (event))
4546
);
4647

4748
if (!(response instanceof Response)) {

packages/kit/src/runtime/server/page/actions.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { get_status, normalize_error } from '../../../utils/error.js';
55
import { is_form_content_type, negotiate } from '../../../utils/http.js';
66
import { HttpError, Redirect, ActionFailure, SvelteKitError } from '../../control.js';
77
import { handle_error_and_jsonify } from '../utils.js';
8+
import { with_event } from '../../app/server/event.js';
89

910
/** @param {import('@sveltejs/kit').RequestEvent} event */
1011
export function is_action_json_request(event) {
@@ -246,7 +247,7 @@ async function call_action(event, actions) {
246247
);
247248
}
248249

249-
return action(event);
250+
return with_event(event, () => action(event));
250251
}
251252

252253
/** @param {any} data */

packages/kit/src/runtime/server/page/load_data.js

Lines changed: 81 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { DEV } from 'esm-env';
22
import { disable_search, make_trackable } from '../../../utils/url.js';
33
import { validate_depends } from '../../shared.js';
44
import { b64_encode } from '../../utils.js';
5+
import { with_event } from '../../app/server/event.js';
56

67
/**
78
* Calls the user's server `load` function.
@@ -16,7 +17,6 @@ import { b64_encode } from '../../utils.js';
1617
export async function load_server_data({ event, state, node, parent }) {
1718
if (!node?.server) return null;
1819

19-
let done = false;
2020
let is_tracking = true;
2121

2222
const uses = {
@@ -28,6 +28,13 @@ export async function load_server_data({ event, state, node, parent }) {
2828
search_params: new Set()
2929
};
3030

31+
const load = node.server.load;
32+
const slash = node.server.trailingSlash;
33+
34+
if (!load) {
35+
return { type: 'data', data: null, uses, slash };
36+
}
37+
3138
const url = make_trackable(
3239
event.url,
3340
() => {
@@ -58,92 +65,96 @@ export async function load_server_data({ event, state, node, parent }) {
5865
disable_search(url);
5966
}
6067

61-
const result = await node.server.load?.call(null, {
62-
...event,
63-
fetch: (info, init) => {
64-
const url = new URL(info instanceof Request ? info.url : info, event.url);
68+
let done = false;
6569

66-
if (DEV && done && !uses.dependencies.has(url.href)) {
67-
console.warn(
68-
`${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`
69-
);
70-
}
70+
const result = await with_event(event, () =>
71+
load.call(null, {
72+
...event,
73+
fetch: (info, init) => {
74+
const url = new URL(info instanceof Request ? info.url : info, event.url);
7175

72-
// Note: server fetches are not added to uses.depends due to security concerns
73-
return event.fetch(info, init);
74-
},
75-
/** @param {string[]} deps */
76-
depends: (...deps) => {
77-
for (const dep of deps) {
78-
const { href } = new URL(dep, event.url);
76+
if (DEV && done && !uses.dependencies.has(url.href)) {
77+
console.warn(
78+
`${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`
79+
);
80+
}
7981

80-
if (DEV) {
81-
validate_depends(node.server_id || 'missing route ID', dep);
82+
// Note: server fetches are not added to uses.depends due to security concerns
83+
return event.fetch(info, init);
84+
},
85+
/** @param {string[]} deps */
86+
depends: (...deps) => {
87+
for (const dep of deps) {
88+
const { href } = new URL(dep, event.url);
89+
90+
if (DEV) {
91+
validate_depends(node.server_id || 'missing route ID', dep);
92+
93+
if (done && !uses.dependencies.has(href)) {
94+
console.warn(
95+
`${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`
96+
);
97+
}
98+
}
8299

83-
if (done && !uses.dependencies.has(href)) {
100+
uses.dependencies.add(href);
101+
}
102+
},
103+
params: new Proxy(event.params, {
104+
get: (target, key) => {
105+
if (DEV && done && typeof key === 'string' && !uses.params.has(key)) {
84106
console.warn(
85-
`${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`
107+
`${node.server_id}: Accessing \`params.${String(
108+
key
109+
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes`
86110
);
87111
}
88-
}
89112

90-
uses.dependencies.add(href);
91-
}
92-
},
93-
params: new Proxy(event.params, {
94-
get: (target, key) => {
95-
if (DEV && done && typeof key === 'string' && !uses.params.has(key)) {
113+
if (is_tracking) {
114+
uses.params.add(key);
115+
}
116+
return target[/** @type {string} */ (key)];
117+
}
118+
}),
119+
parent: async () => {
120+
if (DEV && done && !uses.parent) {
96121
console.warn(
97-
`${node.server_id}: Accessing \`params.${String(
98-
key
99-
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the param changes`
122+
`${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`
100123
);
101124
}
102125

103126
if (is_tracking) {
104-
uses.params.add(key);
127+
uses.parent = true;
105128
}
106-
return target[/** @type {string} */ (key)];
107-
}
108-
}),
109-
parent: async () => {
110-
if (DEV && done && !uses.parent) {
111-
console.warn(
112-
`${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`
113-
);
114-
}
129+
return parent();
130+
},
131+
route: new Proxy(event.route, {
132+
get: (target, key) => {
133+
if (DEV && done && typeof key === 'string' && !uses.route) {
134+
console.warn(
135+
`${node.server_id}: Accessing \`route.${String(
136+
key
137+
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes`
138+
);
139+
}
115140

116-
if (is_tracking) {
117-
uses.parent = true;
118-
}
119-
return parent();
120-
},
121-
route: new Proxy(event.route, {
122-
get: (target, key) => {
123-
if (DEV && done && typeof key === 'string' && !uses.route) {
124-
console.warn(
125-
`${node.server_id}: Accessing \`route.${String(
126-
key
127-
)}\` in a promise handler after \`load(...)\` has returned will not cause the function to re-run when the route changes`
128-
);
141+
if (is_tracking) {
142+
uses.route = true;
143+
}
144+
return target[/** @type {'id'} */ (key)];
129145
}
130-
131-
if (is_tracking) {
132-
uses.route = true;
146+
}),
147+
url,
148+
untrack(fn) {
149+
is_tracking = false;
150+
try {
151+
return fn();
152+
} finally {
153+
is_tracking = true;
133154
}
134-
return target[/** @type {'id'} */ (key)];
135155
}
136-
}),
137-
url,
138-
untrack(fn) {
139-
is_tracking = false;
140-
try {
141-
return fn();
142-
} finally {
143-
is_tracking = true;
144-
}
145-
}
146-
});
156+
})
157+
);
147158

148159
if (__SVELTEKIT_DEV__) {
149160
validate_load_response(result, node.server_id);
@@ -155,7 +166,7 @@ export async function load_server_data({ event, state, node, parent }) {
155166
type: 'data',
156167
data: result ?? null,
157168
uses,
158-
slash: node.server.trailingSlash
169+
slash
159170
};
160171
}
161172

0 commit comments

Comments
 (0)