Skip to content

Conversation

@Rich-Harris
Copy link
Member

@Rich-Harris Rich-Harris commented Mar 14, 2025

This adds a getRequestEvent function to $app/server, which allows you to get... the current RequestEvent:

import { getRequestEvent } from '$app/server';

export function load(event) {
  console.log(event === getRequestEvent()); // true
}

The value of this becomes slightly clearer when you have logic shared between multiple load functions (and/or hooks and actions). For example, you might have an API client that needs access to event.fetch to make credentialled requests from the server. Today, you might do something like this:

// src/lib/server/api.ts
import { API } from '$env/static/private';

export function get(path, { fetch }) {
  const response = await fetch(`${API}/${path}`);
  return await response.json();
}
// src/routes/blog/[slug]/+page.server.ts
import { get } from '$lib/server/api';

export async function load({ fetch, params }) {
  const post = await get(`posts/${params.slug}`, { fetch });

  return {
    post
  };
}

With getRequestEvent, you can make the call simpler:

// src/lib/server/api.ts
import { API } from '$env/static/private';
+import { getRequestEvent } from '$app/server';

-export function get(path, { fetch }) {
+export function get(path) {
+ const { fetch } = getRequestEvent();
  const response = await fetch(`${API}/${path}`);
  return await response.json();
}
// src/routes/blog/[slug]/+page.server.ts
import { get } from '$lib/server/api';

-export async function load({ fetch, params }) {
- const post = await get(`posts/${params.slug}`, { fetch });
+export async function load({ params }) {
+ const post = await get(`posts/${params.slug}`);

  return {
    post
  };
}

It's also useful if you need to do auth stuff. For example we can make a utility function that redirects the user to /login if they're not logged in already:

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 location = new URL('/login', url.origin);
    location.searchParams.set('redirectTo', url.pathname);

    redirect(307, location);
  }

  return locals.user;
}

Then, any time you're creating a load function that requires the user to be logged in, you can do this...

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
}

Today, most server runtimes support AsyncLocalStorage. In these runtimes, you can call getRequestEvent anywhere, including after an await:

export function load() {
  // calling `getRequestEvent` synchronously works everywhere
  const e1 = getRequestEvent();

  await doSomeWork();

  // calling it after an `await` works in environments that support `AsyncLocalStorage`,
  // which is basically all of them
  const e2 = getRequestEvent();

  console.log(e1 === e2); // true
}

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

@changeset-bot
Copy link

changeset-bot bot commented Mar 14, 2025

🦋 Changeset detected

Latest commit: ddc2381

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@benmccann
Copy link
Member

We should probably mention this in the docs somewhere (i.e. the prose and not just the reference docs)

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

@Rich-Harris
Copy link
Member Author

We should probably mention this in the docs somewhere (i.e. the prose and not just the reference docs)

any thoughts on where it would go? can't find a natural space for it

@svelte-docs-bot
Copy link

Copy link
Member

@dummdidumm dummdidumm left a comment

Choose a reason for hiding this comment

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

As for "where make this appear" - I would add it within a section of "loading data".

@Conduitry
Copy link
Member

I'm not up on what the different runtimes provide, but is node:async_hooks pretty standard across all of them? I thought at least one of our official adapter targets didn't really make any attempt to provide Node-compatible modules. Am I just out of date?

@jdgamble555
Copy link

Can you call getRequestEvent() anywhere on the server?

If I have have functions inside of functions in a load function, I have to prop drill (on the server only) the parent function, path function etc in order to have access to them.

This would really simplify that.

J

@Rich-Harris
Copy link
Member Author

is node:async_hooks pretty standard across all of them?

It's supported in Bun, Deno (which I assume covers Netlify functions) and Cloudflare Workers (might need to add nodejs_compat to wrangler config, not sure what's supported by default; we can augment the docs/error message if necessary though I figured we could keep the initial PR lean). I think these are all the ones we care about, except WebContainers (which I'm told need AsyncContext to land).

Can you call getRequestEvent() anywhere on the server?

That's what this is, yep! What's important is when the function is called (i.e. during load etc), not where.

@Rich-Harris
Copy link
Member Author

Added some docs, though I can't link to the preview because it won't deploy — the types will be wrong until this is released

@david-plugge
Copy link
Contributor

david-plugge commented Mar 16, 2025

Is there a plan to make this feature available to shared load functions aswell? On the server the logic could be copied while on the client the event should be a singleton anyway, right?

@Rich-Harris
Copy link
Member Author

Imagine you had two routes, /foo and /bar, with identical load functions:

// src/routes/foo/+page.js
import { getNavigationEvent } from '$app/navigation';

export async function load() {
  await sleep(1000);
  console.log(getNavigationEvent().url.pathname);
}

The user clicks a link to /foo, but after a few hundred milliseconds changes their mind and clicks a link to /bar instead. Since we can't abort an existing load (we could pass around an AbortSignal but the developer would still need to use it), the 'current event' would be for /bar by the time the first load logged url.pathname, meaning "/bar" would get logged both times.

In other words this would result in subtle bugs — often harmless, but not always.

So for this to behave correctly one of two things would need to happen:

  • you would need to call getNavigationEvent synchronously (which is a fairly significant limitation)
  • browsers need to ship AsyncContext

Given that, I don't think we should offer a universal version of this function at this time.

@david-plugge
Copy link
Contributor

Yep, that makes sense. Would love to see AsyncContext everywhere. Thanks for clarifying!

@jdgamble555
Copy link

I understand if this would require a huge rewrite, but I figured I would ask. Since Svelte itself is a compiler, why couldn't Svelte(Kit) itself compile the event to be either a global or automatic prop drilled using the parser:

From:

export const load: PageLoad = async () => {
    await new Promise(r => setTimeout(r, 1000));
    await childFunction(); // may or may not be an async func
};

const childFunction  = async () => {
    console.log(getNavigationEvent().url.pathname);
}

Compiled to:

export const load: PageLoad = async (event) => {
    const childFunction  = async () => {
        console.log(event.url.pathname);
    }
    await new Promise(r => setTimeout(r, 1000));
    await childFunction(); // may or may not be an async func
};

Or compiled to:

// or maybe something like this which is perhaps more logical
// automatic prop drilling
export const load: PageLoad = async (event) => {
    await new Promise(r => setTimeout(r, 1000));
    await childFunction(event);
};

const childFunction = async (event) => {
    console.log(event.url.pathname);
}

There would be no need for an actual context, as the event would be available anywhere:

  1. Because it is compiled to be a global
  2. Or because it is automatically prop drilled

I imagine this is extremely complex and changes things, but it is theoretically possible.

I am first to admit I have a minimum understanding of the compiler.

J

@adiguba
Copy link

adiguba commented Mar 16, 2025

@jdgamble555 : I think this is only possible if the two function are on the same file...

But above all, I don't really see the point since we can simply use a parameter :

export const load: PageLoad = async (event) => {
    await new Promise(r => setTimeout(r, 1000));
    await childFunction(event); // may or may not be an async func
};

const childFunction  = async (event) => {
    console.log(event.url.pathname);
}

@jdgamble555
Copy link

The same reason we have context in the first place, to avoid passing it as a parameter to nested components (prop drilling).

In a real app, I am not going to have a child component, I'm going to have nested child components.

export const load: PageLoad = async () => {
    const posts = await getPosts();
    return {
      posts
   };
};

const getPosts  = async () => {
    const user = await getUser();
    return await getPostsByUserId(user.id);
}

const getUser = async () => {
    // you may have locals setup in hooks.(server).ts i.e.
    const { locals: { db } } = getRequestEvent();
    return db.user;
};

Is this not the reason for Context in the first place?

Obviously this is incredibly simplified, but once your app gets complex enough, you will have nested functions for the single responsibility principle and to keep things clean.


That being said, I do see your point. There would be no way for the parent to know that the child function uses an event if it is in a different file.

Is it possible to build a svelte compiled browser version of AsyncContext until JS supports it? Maybe using WeakMap or something similar?

Thanks,

J

@Rich-Harris
Copy link
Member Author

It will make code behave strangely. Even something as simple as this would break:

function stuff(...args) {
  const event = getRequestEvent();
  console.log(args);
}

It's never worth it. Transformations should be as simple and predictable as possible.

@teemingc teemingc added the feature / enhancement New feature or request label Mar 17, 2025
@adiguba
Copy link

adiguba commented Mar 17, 2025

Hi,

I think getRequestEvent() can replace getContext() on /kit/src/runtime/app/state/server.js, which would allow page from $app/state to be more widely accessible.

No sure if it should be done here, or on a separate PR.

@Rich-Harris
Copy link
Member Author

That's an interesting idea. I think it ought to be a separate discussion, because there's some tricky nuance around what (for example) page.data and page.status should be when we haven't finished loading

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

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

LGTM

@Rich-Harris Rich-Harris merged commit d1c0c68 into main Mar 17, 2025
18 checks passed
@Rich-Harris Rich-Harris deleted the get-request-event branch March 17, 2025 15:27
@github-actions github-actions bot mentioned this pull request Mar 17, 2025
@d3rpp
Copy link

d3rpp commented Mar 17, 2025

This is going to save me so much complexity thank you so much for this you absolute legend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature / enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.