Server-side fetch request should have headers
#696
Development PRs
See #696. Not quite sure what the correct behaviour should be here
This PR base on #1417 (comment) and on all discussion for direct external API requests (#1417) and headers manipulation from #696.
Basically, now we provide developers full control of how to retrieve data on the server-side and even UNIX sockets can be an option. The test implementation is a little tricky but looks like we have already a similar thing for handles large responses.
Thanks.
Fixes: #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
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
What does this change?
Resolves issue #1337
Als je op de lessons-pagina een playlist uit de suggestiesectie liket, dan verplaatste de playlist zich voorheen niet meteen naar de selectie gelikete playlists. De gebruiker moest eerst de pagina herladen (ook andersom, van de gelikete naar de suggesties). Ik heb er nu voor gezorgd dat wanneer een playlist wordt geliked of unliked, de selecties automatisch worden geüpdatet, zodat de gebruiker de pagina niet steeds hoeft te herladen. livesite
How Has This Been Tested?
- User test
- Accessibility test
- Performance test
- Responsive Design test
- Device test
- Browser test
Images
How to review
Als de gebruiker nu een playlist uit suggesties liked wordt de lijst automatisch geüpdatet en verschijnt de gelikete playlist direct in de liked playlist selectie op de lessons pagina.
Issue
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 Info
Is this the same issue as #672? Although I suppose it covers a wider range (all headers) instead of just cookies.
This is an interesting and tricky problem. The browser's fetch adds a number of implicit headers:
{
host: 'localhost:3000',
connection: 'keep-alive',
'sec-ch-ua': '',
'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/90.0.4421.0 Safari/537.36',
accept: '*/*',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:3000/load/headers',
'accept-encoding': 'gzip, deflate, br'
}
Meanwhile, the fetch function available to load in SSR can read the page's request headers, which look something like this:
{
host: 'localhost:3000',
connection: 'keep-alive',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/90.0.4421.0 Safari/537.36',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'navigate',
'sec-fetch-user': '?1',
'sec-fetch-dest': 'document',
referer: 'http://localhost:3000/load',
'accept-encoding': 'gzip, deflate, br'
}
We could simulate the default fetch headers, up to a point:
const implicit_headers = {
host: request.headers.host,
connection: request.headers.connection,
'sec-ch-ua': ???,
'sec-ch-ua-mobile': ???,
'user-agent': request.headers['user-agent'], // or should this be a SvelteKit UA?
accept: '*/*',
'sec-fetch-site': 'same-origin', // vary according to whether we're fetching a local endpoint or not
'sec-fetch-mode': 'cors', // vary according to `opts.mode`
'sec-fetch-dest': 'empty',
referer: ???, // not sure how to reliably determine this
'accept-encoding': 'gzip, deflate, br' // presumably whatever node-fetch can handle?
};
// merging is actually more complicated than this, can't override `host` etc. but you get the idea
const headers = { ...implicit_headers, opts.headers };
As you can see there are a number of things that aren't totally obvious — the sec-ch-ua* stuff (maybe we can omit this stuff, it's a ), user-agent, and referer.
Presumably there could also be custom headers injected by whatever layers sit between the user and the app, like x-forwarded-host and so on. Is it acceptable to just pass through request.headers with a few modifications to remove things that are specific to the original page request, like sec-fetch-mode and accept? Feels dangerous, but maybe?
You're right, this is a bit tricky. I would suggest following the principle of least surprise and pass the headers as they are, without adding and especially without omitting anything, unless for a good reason.
This means that both local fetch and remote fetch should have user-agent from the original request, not node-fetch or SvelteKit UA. All custom headers, of which there can be many, should be passed over as well, as the endpoint may rely on them (in my case, I expect function-execution-id, x-appengine-user-ip, x-country-code to be there).
Furthermore, I think that it's better to not make changes to the headers, unless there is a well defined reason for it.
There may be an actual need to adjust and align the headers though, and I foresee that it would be different depending on whether the fetch is internal or external. Some headers may need to be defined implicitly, some removed, some replaced.
The simplest starting point would be this:
const implicit_headers = {
referer: request.url // maybe request object should have original request url so that there is no need to
}
const replace_headers = { }
const headers = { ...implicit_headers, ...request.headers, ...replace_headers, ...opts.headers }Would this issue also be affecting cookies being sent on regular page routes? For example, I am not explicitly calling fetch, I just have a page in my project (/src/routes/profile/index.svelte) and when I check the headers server side (/src/hooks.js) in getContext the headers.cookies is empty. In the docs below, cookies is empty even though they are set in the browser. Iv confirmed the browser is not sending them when navigating to a route.
@BraydenGirard that's a TODO that should be easy to fix, though yeah, it's really just a more tightly-scoped version of this issue #672 (comment)
I have solved my issue here, when creating cookies using the npm cookie module, you have to specify the path as "/" if you want the cookie to apply to all routes. By default the path gets set to the endpoint (in my case "/auth") that you are returning the cookie in.
Oh, that's good to know — thanks
I think I just ran into this, and since I'm using firebase-admin's cookie-token authentication, I'm unable to successfully call any of my endpoints my data in load. Would it be a reasonable workaround to just do my fetch in an onMount handler? Or is there a better way to handle this while this is unresolved?
fetch in onMount is a reasonable workaround, yeah. We'll get this fixed soon though, it's a priority
FYI the credentials thing is fixed (#835), we just need to nail down what the correct behaviour should be for other headers
While the topic of headers is in flux, I suggest considering the following:
- Remove
hostHeaderoption in favor of adapter config options
Config has the propertyhostHeader, which gives special treatment to one header. It's understandable why it's there, but I think it's a narrow solution because there are other forwarded headers.
In Express you can set "trust proxy" option, which has many possible configurations. Similar option in Fastify.
I think this configuration belongs to the adapter and instead of specifying hostHeader in kit config, it would be better to be able to pass it as an option to the adapter:
kit: {
adapter: node({ trustProxy: true }),
}
A litmus test on whether an option belongs to adapter config or kit config, would be to ask whether the option becomes irrelevant when you switch the adapter to static.
- It is important to not lose any headers. For example, AppEngine provides the following headers:
{
"x-appengine-city": "phuket",
"x-appengine-country": "TH",
"x-appengine-citylatlong": "7.880448,98.392250",
}
These headers are platform-specific but useful for customizing responses for users.
I'd like to just throw in a quick thing I haven't seen mentioned. We're doing our fetching via Apollo. Which unfortunately requires a full URI in their configuration. Meaning a relative url in the fetch calls isn't do-able and in turn we've had a rough time trying to find a work around to get our cookie headers to be available in the initial server request. So while the relative fetch path seems like a good fix. it might not be compatible with things like Apollo.
Am I correct to infer that the issue is de-prioritised? (I base this inference on the removal of the issue from the 1.0 milestone and the addition of the p2-nice-to-have label.
(If no progress is expected any time soon, my team would need to move data fetching for our app into onMount(), although I strongly prefer server-side fetching.)
Thank you.
Without this feature I have a tree of components that need to wait for a follow up request before they can be rendered. The follow up request fetches the auth token from an endpoint. It slows the performance of the site down a lot, and also increases the complexity of my code.
I created a PR with simple change that adds headers from parent request to fetch on server side. Those headers can be overridden by specifying new ones in the fetch request, so it should be backwards compatible. Please take a look: #2911
Hey, I have a issue which might be related to this. Before I open a new issue I might ask you guys.
On the function below I get the ERROR (but works in dev mode):
> Using @sveltejs/adapter-static
> body used already for:
at consumeBody (file:///C:/Users/JvG/Documents/repos/myrepos/hubgermanyweb/node_modules/@sveltejs/kit/dist/install-fetch.js:4912:9)
at Response.text (file:///C:/Users/JvG/Documents/repos/myrepos/hubgermanyweb/node_modules/@sveltejs/kit/dist/install-fetch.js:4877:24)
at visit (file:///C:/Users/JvG/Documents/repos/myrepos/hubgermanyweb/node_modules/@sveltejs/kit/dist/chunks/index5.js:490:39)
at processTicksAndRejections (node:internal/process/task_queues:94:5
But when I replace my fetched URL with the full domain https://mywebsite/api/content it works just fine. I tested this with different node versions and so on. But all this worked the last months fine. I just appeared now. I also tried find a earlier svelte and static adatper version since it worked before but I was not successful to do so.
My header is a very basic one:
let myHeaders = new Headers(); myHeaders.append('Content-Type', 'application/json');
import { localFetch } from '.....'; // localFetch(svelte fetch, url, callbackParseFunction)
export async function load({ fetch }) {
let data;
try {
// only works when writing: localFetch(fetch, "https://mywebsite/api/content" , parseData);
data = await localFetch(fetch, "/api/content" , parseData);
} catch (error) {
console.log(error);
}
return {
props: {
dealsData: data
}
};
}
Any idea if this is related or why this is happening? Thanks in advance! I could not find any related issue yet.
I just wanted summarize things, from what I understand:
There's two open PRs, #2911 and #3631 that look pretty much topologically the same to me, they both merge in all the headers from the incoming request event into the outgoing server-side fetch call.
I'm not sure how #835 would have fixed the credentials thing, seems like the code change needs to happen in runtime/server/page/load_node.js
My two cents: If the goal is to just get credentials included, credentials as defined in https://fetch.spec.whatwg.org/#credentials, we should just copy the "cookie" header and not all the headers. Edit: I guess HTTP basic auth means it should also do the Authorization header.
For me, other headers like User-Agent and Accept were also important to return the right kind of response.
closed via #3631 — thanks everyone!
For clarity: we eventually opted to just forward all headers except cookie and authorization (which are added back if appropriate), plus if-none-match. The referer header is overridden to be the page URL.
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 ;)