Make event available to page load() function
#3503
event available to page load() functionDevelopment PRs
Server-side fetch in load function will include original request headers. (#696)
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 testand lint the project withpnpm lintandpnpm check
Changesets
- If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running
pnpx changesetand following the prompts. All changesets should bepatchuntil SvelteKit 1.0
Fixes #3503 Fixes #696 Reverts #3631 Reverts #835
Okay this is the last one of my SvelteKit wishlist items. Not sure what you'll think but it certainly makes sense to me.
This does two things, each in its own commit.
- Exposes
eventto the load() function - Reverts and deletes all the logic related to passing headers and cookies to the upstream service calls.
#3631 was clearly a mistake. If we have a network call graph that looks something like this:

It doesn't make any sense for the Node service to pretend to be a browser. Some of the reasons:
- You're lying about the user agent. If you passed one at all it should be, perhaps,
node/sveltekitor something - You're passing headers unidirectionally, sending them out but but not sending the response headers back to the browser, because of course that wouldn't make any sense. The calls are many to one so what should you do, merge them?
- The type is different (HTML vs most likely JSON), so the
acceptheader could be wrong - Some of the headers are nonsensical for service-to-service calls, like
refererwhich is a browser concern
There just seems to be some confusion and it seems like people are considering calls A-C in the graph to be the Z call proxied onward. When in fact those calls are unrelated to call Z, except that call Z is the reason calls A-C are being made.
But then even with all the complexity, we still can't pass along the most important information: the auth cookies. To get the full details of the incoming request you have to use an endpoint (or a hook). Endpoints should be a feature you can use if you want but shouldn't be mandatory.
In a more ops-driven, "close to the metal" kind of enterprise setup, our frontend services live among a constellation of other services, both in front as reverse proxies, and behind as upstream services. In this world things like SSL termination and endpoints are simply not wanted or needed. In this world if we want to avoid the moving target of CORS and serve our data on the same domain as our HTML, we simply put a reverse proxy in front and split traffic.
What this change does is restores control. If you want to call your backend services and spoof like you're a browser, you can do so! Copy all your headers over. If you want to forward a backend service's "set-cookie" directive to the browser, go ahead. But if you do nothing the calls to the backends get no headers, just as if you made a curl call, which seems like a pretty reasonable thing to do by default.
Here's some examples:
export default async function load({ event, params, fetch }) {
let response = await fetch('https://a.backend.com/data/' + event.id, {
headers: {
// Use end user credentials
// You could parse the cookie string and slice out a specific auth cookie, or
// or just pass the whole darn thing.
cookie: event?.request.headers.get('cookie'), // only relevant server-side
credentials: 'include', // only relevant client-side
},
});
...
}
If your backend sets a cookie and you want to set the same cookie in the browser, there's no way to do that explicitly, and I stripped out the automatic logic for that because, hey, making service calls shouldn't have side effects. You get the data, you do what you want with the data, end of story. So I extended some interfaces with a set_cookies string array.
export default async function load({ event, params, fetch }) {
let response = await fetch('https://a.backend.com/data/' + event.id, {
headers: {
cookie: event?.request.headers.get('cookie'), // only relevant server-side
credentials: 'include', // only relevant client-side
},
});
let data = await response.json();
let set_cookie = response.headers.get('set-cookie');
return {
props: {
widgets: data.widgets
},
set_cookies: set_cookie && [set_cookie],
}
More rationale: Even in the demo TODO app, you need access to the user's cookies to make the backend call. I know that backend is slated for removal in #4264 but as long as it remains it does prove a point. In my repro case for #4902 I have code like this:
johnnysprinkles/sveltekit_after_navigate_bug@53dd01a (comment)
where I have to put the userid in session because there's no way to read it from the request in load(). If you don't want to put it in session and you don't want to use an endpoint, you're currently out of luck.
Anyway, I hope this might at least provoke some discussion. I know this goes against a SvelteKit value of abstracting away the differences between client and server, but that abstraction is kind of a fiction anyway.
Also, I have no idea how much this would break. Shadow endpoints? Various adapters?
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 testand lint the project withpnpm lintandpnpm check
Changesets
- If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running
pnpm changesetand following the prompts. All changesets should bepatchuntil SvelteKit 1.0
Issue
Describe the problem
One common scenario in SSR rendered apps is that the initial render makes some async API calls, then later on when the user is interacting with the page, calls are made to that same API directly from the browser. For the server initiated API calls, it can simply forward on the end user's auth cookies. This is generally a good pattern as it makes your frontend service a dumb and unprivileged middleman.
Say we have a load function like this in one of our route components:
export async function load({ fetch }) {
let response = await fetch('https://api.example.com/mylatest', {
credentials: 'include',
headers: {
cookie: 'jwt_token=???',
},
});
}
There doesn't seem to be a way to get at the end user's incoming request. This isn't specific to fetch or cookies, more generally we need the full detail of the request to service it properly. Maybe we look at the IP and do something different based on a coarse geolocation. Maybe we look at cookies, not just for forwarded authentication but even just for display preferences. Maybe we look at user-agent and fetch a smaller dataset if it's a phone than if it's a desktop. OK these examples are a bit contrived, it is 99.9% about forward auth.
So I think what SvelteKit wants you to do is use hooks to examine the low-level details of the request and turn it into higher level details that go in locals. So I can use handle() and put stuff in event.locals, but unless I'm missing something, there's no way to get that from load(), you can only get that from a data endpoint.
One workaround that is serviceable is to put whatever you need in session. But then it leaks out to the client (clearly visible with cmd-U show source), when it's actually only needed server-side.
So I'd propose simply plumbing locals through into the load() functions.
Describe the proposed solution
See above.
Alternatives considered
See above. Edit: See below, maybe just update externalFetch?
Importance
would make my life easier
Additional Information
No response
Info
Or, what might actually make more sense is to make "incoming_request" available to externalFetch.
A one-liner here would do it (verified locally): https://github.com/sveltejs/kit/blob/ee906a3b2b103e4523eb1b6bb13f04d03eb35238/packages/kit/src/runtime/server/page/load_node.js#L204
Changed to:
response = await options.hooks.externalFetch.call(null, external_request, event);
Also I think this might end up resolving several issues related to server-side data fetching for people who can't or don't want to have their API as a subdomain of their website.
locals not being sent to load is by design. locals is intended for things that you don't necessarily want available to the browser, and everything passed to a load function would also be sent to the browser. If you want to expose certain data to a load function, return it from getSession.
I'm not sure what passing event to externalFetch would specifically address, but having it be called differently from load and from other fetches sounds odd to me.
That makes sense, SvelteKit does try to present a symmetry between client-side and server-side. If we had locals or even the whole request event available in load(), on the client it would just get null or empty object which might be weird.
Still though, what about externalFetch? It's one of the few places we break the client/server symmetry and seems like the right place. I'll re-title this issue as "Make request event available to externalFetch()".
The main thing this would allow is forwarding auth cookies, such as:
export async function externalFetch(request, event) {
let cookies = cookie.parse(event.request.headers.get('cookie') || '');
request.headers.set('cookie', `JWT=${cookies.JWT}`);
// Or blindly send all cookies...
// request.headers.set('cookie', event.request.headers.get('cookie'))
// Or pass auth a different way
// request.headers.set('authorization', `bearer ${cookies.JWT}`);
return fetch(request);
}
This provides an out if the automatic cookie passthrough doesn't fit your needs (which it won't if you have an API that's not a subdomain of your website, or if you occasionally want to point your dev frontend at a prod backend, etc). And I think it may end up resolving #3160 . Not sure about #672 and #1777.
Ah, reading the docs more I think the expected thing is, if you wanted to use APIs on another domain you'd create endpoints for that. It sounds tedious but maybe with rest params you could make it a single endpoint. It would add a network hop though, if you used that endpoint on the client as well as the server which seems to be what's intended. Also, you'd need to make up a path hierarchy that doesn't collide with the paths used for pages, I guess generally by using /api/[...] and stripping the api prefix to send the rest of the path to your actual API.
I mean really it seems simpler to just allow things to go direct to the API (via node-fetch) in the server-side case, and also direct using CORS in the client-side case (which we can already do). Curious if anyone else would want this.
But back to that automatic cookie passthrough logic... the fact that we even have that suggests it's ok to call APIs where they lie, instead of shuttled through a local endpoint.
I can see it's emulating what would happen for a client-made fetch call, the only problem is it's emulating what would happen for the "did nothing for CORS" case. So now, the client fetch calls are actually more capable than the server ones because they can call any domain with cookies and auth. This seems incongruous.
Why don't we simply support {credentials: 'include'} like the client would, support it in spirit anyway. Well, more like "abuse" it the same way Svelte abuses labeled statements. It would be a small code change like this, I'd be happy to send a PR.
Hmm, #3631 didn't end up including credentials headers (cookie, authorization) after all, so re-opening this.
I do see #835 but that only deals with internal fetch calls, maybe I'm missing some discussion on why they decided not to do external as well?
The fetch that server-side load gets tries to simulate what the browser's own fetch would do, based on what it has available to it.
- If the cookies live on your app's domain, server-side
loadfetchwon't pass them along to another domain they're not for. This would be a big security issue, and isn't how browsers behave. - If the cookies live on that external domain, the server wasn't even sent them to begin with, so it can't forward them on to that domain in SSR. This is a limitation of the definition of SSR, how browsers behave, and the cookie spec, and isn't something that SvelteKit can fix.
- We don't want to make the whole request object available in
load, as that (among other things) defeats the purpose of HTTP-only cookies that can't be accessed in JS.
If you want the cookies to live on your app's domain, you can use a local endpoint to forward them to the remote domain, so that this will work both on the server and on the client. If you want the cookies to live on the external domain, you won't be able to use SSR for this request, and you will need to rewrite this portion to always use a client-side fetch, without the load function.
Cool, leaving as closed but going to re-title to previous title.
Just to respond to a couple points... If the client sends a cookie to the server the server can do whatever it wants with it, including using it to make external service calls. That's not a security issue at all. But I can see how doing that with credentials: include would make it way too automatic and easy to do inadvertently. So really exposing the event as a second parameter to externalFetch seems like the best option.
No problem for me at the moment since when I run local I have my website on localhost:3000 and my API on localhost:4000, and remote I have api.mysite.com and mysite.com. If I ever moved my API to not be a subdomain then this would be an issue and the options currently are:
- Put it in session and leak it to the client
- Make an endpoint intermediary
- Fork SvelteKit
OK, I've thought a little more about this... my case might be usual because I'd be setting the same auth cookie on my frontend domain and my backend domain. That way ajax calls will send it to mybackend.com, but it also reaches SvelteKit node service on myfrontend.com, and from there I'd want to use it on upstream fetch() calls in my load() function. This might be an esoteric use case. And also this might all be moot anyway if third party cookies are going away. I'm satisfied on closing this issue.
I'm having a similar issue to this, I would like to use the load fetch to pass credentials to my auth service for validation... I have 2 subdomains:
auth.domain.comdashboard.domain.com
The auth server issues a cookie with the domain set to .domain.com, however because of the follow code:
The cookie is left out when calling from the server side. This is not exactly the same issue as @johnnysprinkles, however I feel like my use-case is pretty standard. Could we make the code above slightly more clever by:
- getting the cookie outside of the if block
- if the cookie exists and credentials need to be included, potentially parse out the domain
- use the parsed cookie domain as a precursor to the existing check
This is kind of what I'm thinking...
const cookie = event.request.headers.get('cookie');
if (cookie && opts.credentials !== 'omit') {
const domain = cookie.match(/domain=([^;]+)/i);
if (
domain &&
`.${new URL(requested).hostname}`.endsWith(domain[1]) ||
`.${new URL(requested).hostname}`.endsWith(`.${event.url.hostname}`)
) {
uses_credentials = true;
opts.headers.set('cookie', cookie);
}
}
I'm happy to PR this change if necessary, but I want confirmation that I'm not expecting something which isn't idiomatic
Edit: This obviously falls down in the case of multiple cookies, which I failed to realise 🤦
Edit 2: I got round this by, as @Conduitry suggested, creating a proxy endpoint on dashboard.domain.com and forwarding the request with cookies onto auth.domain.com
Thinking a little more about this I'm back to my original thought... if the request event is available to load() that code can use request data however it wants to form outgoing requests. I don't think we'd want to do anything automatically to ferry cookies across into fetch or even to do anything in externalFetch since that would apply to all outgoing calls. I think we'd want to make it very explicit, as in my initial description.
I still think it's a good idea to add event as a parameter to load(). Yes, the client would see null there but you can't completely hide the fact that client and server are different.
The problem I am having with my project, is that we have a load balancer at the beginning of the request that sets the authorization header from an auth service that is valid for the life time of the request. This token is set in the authorization header and is valid for every microservice behind the load balancer including the Svelte servers.
I thought it would be nice if when we were server side rendering instead of having to go through the load balancer again and reauthenticate we could just pass the Auth header to the externalFetch. But, Svelte does not pass the Auth Header at all. The hack I have used to get around it is setting a temp header in handle and resetting it to Auth in external fetch like so.
export const handle = async ({ event, resolve }) => {
event.request.headers.set('Temp-Authorization', event.request.headers.get('Authorization'));
const response = await resolve(event);
return response;
};
export async function externalFetch(request) {
request.headers.set('Authorization', request.headers.get('Temp-Authorization'));
return fetch(request);
}
I know there is a security risk in the fact we are sending the token with every external request but, is that the only risk? I totally get that this is probably why Sveltekit doesn't pass on those headers, and that we have a weird use case. I just wish there was a way I could configure what headers are passed to external fetch with like a white list policy or something based on what endpoints we have.
Oh right, I think Rich did make it include all the incoming request headers on all outgoing requests via fetch (except for the auth ones). This seems weird to me honestly, seems like you'd want to explicitly set the headers on the services you call. Another thing in the name of "try our damnedest to main the illusion of symmetry between client and server."
Cool hack though, better than putting it in session and leaking it to the client. You could do it conditionally based on the destination URL even.
@johnnysprinkles Thanks dude. Totally agree we have a weird use case. Was just trying to think of a robust solution to my problem that wouldn't make Sveltekit worse but it probably would lol. Was thinking maybe a policy config, or we could pass all headers if it's a subdomain like we pass the cookie.
Probably just going to have to stick with my hack for the time being. We are definitely going to make it conditional on the URL, we just have the concern of more junior devs or a bug accidentally exposing it to the wrong API. Guess that's what PR reviews are for tho lol.
Having the same problem here. Passing the cookie to session feels like a decent workaround. As a side effect, we end up sending the cookie back to the browser in the session, but I don't see an issue with it.
// inside hooks.js
export const getSession: GetSession = (event) => {
const user = event.locals.user;
if (user) {
return {
user: {
....
},
cookie: event.request.headers.get("cookie"),
};
}
return {};
};
// inside component
<script context="module">
export const load = async ({ params, fetch, session }) => {
const response = await fetch(`${HOST}/api/resource/${params.id}`, {
headers: {
cookie: session.cookie
}
})
if (response.ok) {
return {
props: {
res: await res.json()
},
};
}
return {
status: res.status,
error: new Error(`Could not load ${params.id}`),
};
}
</script>The load function in page.js could get the jwt token via page.server.js. Because page.server.js does received the cookie and is able to pass data to page.js. This is document at . Be careful with this as this data will also be available in the browser. For the JWT token from this question it is intended to be public as the browser will also use the token to access the API directly. The page.server.js would look like:
/** @type {import('./$types').PageServerLoad} */
export async function load({ cookies }) {
return {
token: cookies.get('token')
}
}
and the page.js receives the token in the data attribute. Based on the code from the question:
export async function load({ fetch, data }) {
const {token} = data;
let response = await fetch('https://api.example.com/mylatest', {
credentials: 'include',
headers: {
cookie: 'jwt_token='+token,
},
});
}
In my own case I've used it to fetch my graphql api with a Bearer token. In my case the headers will be like:
headers: {
Authorization: `Bearer ${token}`,
},
Just in case somebody was searching for this authorization method.
I've posted a solution earlier to get the JWT token via the page.server.js. However this will exploit the JWT to the browser via hydration. It might not be a issue but I preferred to search for a better solution.
And I've found a cleaner solution. It will use the handleFetch on the server in hooks.server.js to passthrough the cookie for fetch used in load() on the server. And it will use document.cookie to passthrough the cookie for fetch on the browser in case of subsequent data retrieval.
The handleFetch is documented at
For src/hooks.server.js:
import cookie from 'cookie';
/** @type {import('@sveltejs/kit').HandleFetch} */
export async function handleFetch({ event, request, fetch }) {
const { token } = cookie.parse(
event.request.headers.get('cookie') ?? ''
);
const validUrls = ['http://127.0.0.1:5070/api', 'https://example.com/api']
if (token && validUrls.includes(request.url)) {
request = new Request(request,
headers: {
cookie: 'jwt_token='+token,
},
);
}
return fetch(request);
}
And the load function from the original post of the author will be:
import cookie from 'cookie';
export async function load({ fetch }) {
let token = undefined;
const browser = typeof document !== 'undefined';
if (browser) {
const cookies = cookie.parse(document.cookie);
token = cookies && cookies.token;
}
let response = await fetch('https://api.example.com/mylatest', {
credentials: 'include',
headers: {
cookie: 'jwt_token='+token,
},
});
}
Somewhere on the client or server you will need to set the cookie JWT token. For svelte kit it will probably be in an action login page.server.js
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ cookies, request }) => {
.... login ...
cookies.set('token', token, {
maxAge: 12 * 60 * 60, // seconds
path: '/',
httpOnly: false,
});
}Pro tip: You can prefix GitHub URLs of issues, PRs or discussions with svcl.dev/ to view them on this page! Also try it on a GitHub release ;)