Exposing event to load(), and removing the automatic header forwarding
#5195
Closing issues
Fetch call made from the server should have the same headers, as the one made from the client.
<!-- index.svelte -->
<script context="module">
export async function load({ fetch }) {
const { headers } = await fetch('/content.json').then(res => res.json())
return {
props: { headers }
}
};
</script>
// content.json.ts
export const get = (request) => ({
body: {
headers: request.headers // <- headers: {} if requested during SSR, but populated otherwise
}
});
In the above case the request.headers are empty ({}) if requested during SSR, but populated when requested from the client on navigation or if requested directly.
The expected behavior is that in both cases the headers are populated and requests look the same.
envinfo:
npmPackages:
@sveltejs/adapter-node: next => 1.0.0-next.10
@sveltejs/kit: next => 1.0.0-next.60
svelte: ^3.29.0 => 3.35.0 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
Pull request
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
Info
🦋 Changeset detected
Latest commit: ff4e342
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
As I mentioned in #3503 (comment), exposing event to load is bad because you completely lose the security of HTTP-only cookies.
I guess I never quite understood that point... this is a change inside the running NodeJS code, not sure how that could be a security issue. I looked back at your comment and see that the server-side fetch currently tries to simulate what the browser's fetch would do, but why impose those kind of elective limitations on your own code in the serverside?
Speaking of HTTP only cookie though, I was too aggressive in pruning and should have left in the code that strips out set-cookie and etag from the fetched headers. Because that information might not want to be exposed to client logic. We could look at the cookie value itself and look for HttpOnly but for now I just reverted that file back to how it was, so no set-cookie value ever appears on the client via that "serialized fetch result" data passing mechanism. (See update.)
More thoughts about this... if right now we're implementing the browser's XHR security model in very broad strokes, it's interesting to think about what would be involved in implementing it exactly correctly:
- Respect the various "credentials" options, not just "omit"
- Understand and respect all the various
Access-Control-*headers, and there's a lot of them - Know if the request should be preflighted or not
- Change all the rules depending on if it's HTTP or HTTPS
- Keep this all up to date as things change (isn't Chrome always threatening to remove third party cookies altogether?)
- Maybe even use browser sniffing to make the behavior match the user's UA
- Ideally we'd even match older browser versions, keeping a historical record of how CORS works in all browsers across all versions
This doesn't seem like a business we want to be in.
I did a quick search and we have 79 open SvelteKit issues containing "fetch". Just on the first page I see a couple that this PR might resolve. Would be pretty nice to be able to close all those out.
Nice!
[feat] Exposing the RequestEvent from serverside load() invocation
• May 31, 2022, 3:52 AM[breaking] Stop including browser headers on server-side fetch() calls
• May 31, 2022, 4:18 AMPro 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 ;)