-
-
Notifications
You must be signed in to change notification settings - Fork 232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SvelteKit] Error: Cannot use cookies.set(...)
after the response has been generated
#466
Comments
Thanks for the report. The problem here is that sveltekit generates the response when all load functions finished. At this moment the headers are sent, as this has to happen before we can send the response body. src/routes/+page.server.ts import type { PageServerLoad } from './$types';
export const load = (async ({ fetch, locals: { getSession } }) => {
// get the session before load finishes
const session = await getSession();
const loadPosts = async () => {
const response = await fetch('/api/posts', {
headers: {
Authorization: `Bearer ${session?.access_token}`
}
});
const data = await response.json();
return data.posts;
};
return {
lazy: {
posts: loadPosts()
}
};
}) satisfies PageServerLoad; src/routes/api/posts/+server.ts import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
import { createClient } from '@supabase/supabase-js';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ request }) => {
const [_, access_token] = request.headers.get('Authorization')?.split(' ') ?? [];
const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
auth: { autoRefreshToken: false, persistSession: false }
});
supabase.auth.setSession({ access_token, refresh_token: '' });
const { data: posts, error } = await supabase.from('posts').select();
console.log({ posts, error });
return json({
posts
});
}; If you want to lazily load data from supabase without an extra endpoint, you can do it like this: // create a new supabase client with the access token
// this way we don´t have to rely on cookies
function createLazyClient(accessToken: string = '') {
const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
supabase.auth.setSession({
access_token: accessToken,
refresh_token: ''
});
return supabase;
}
export const load = (async ({ locals: { getSession } }) => {
const session = await getSession();
const lazySupabase = createLazyClient(session?.access_token);
const loadPosts = async () => {
// simulate delay
await new Promise((res) => setTimeout(res, 1000));
const { data: posts } = await lazySupabase.from('posts').select();
return posts;
};
return {
lazy: {
posts: loadPosts()
}
};
}) satisfies PageServerLoad; |
Thanks so much @david-plugge for your response. I've tried the 2nd method by initiating a lazySupabase client and then making a streaming promise call within one of my load functions, but unfortunately I still seem to be running into the same issue. Do I need to also change every other instance of Supabase across all my load functions, as well as in my hooks.server.ts? |
No, the helpers should work fine when used outside of lazy loaded promises. |
@david-plugge Here's my +layout.server.ts: import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
import { createClient } from '@supabase/supabase-js';
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
function createLazyClient(accessToken = '') {
const supabase = createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
supabase.auth.setSession({
access_token: accessToken,
refresh_token: ''
});
return supabase;
}
export const load = (async ({ params, locals, fetch }) => {
const session = await locals.getSession();
const lazySupabase = createLazyClient(session?.access_token);
const fetchItineraries = async () => {
const { data, error: err } = await lazySupabase
.from('trips_itineraries')
.select('*')
.eq('trips_id', params.tripId);
if (err) {
throw error(500, err.message);
}
if (!data?.length) {
const response = await fetch('/api/itineraries/generate', {
method: 'POST',
body: JSON.stringify({
destination: 'Paris, France',
days: 1,
preferences: 'must-see attractions'
})
});
const { choices } = await response.json();
const itineraries = JSON.parse(choices[0].message.content);
return itineraries;
}
return data;
};
return {
lazy: {
itineraries: fetchItineraries()
}
};
}) satisfies LayoutServerLoad; Here's my hook.server.ts: import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public';
import { createSupabaseServerClient } from '@supabase/auth-helpers-sveltekit';
import { error, redirect, type Handle } from '@sveltejs/kit';
import { dev } from '$app/environment';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.supabase = createSupabaseServerClient({
supabaseUrl: PUBLIC_SUPABASE_URL,
supabaseKey: PUBLIC_SUPABASE_ANON_KEY,
event,
cookieOptions: {
secure: !dev
}
});
/**
* a little helper that is written for convenience so that instead
* of calling `const { data: { session } } = await supabase.auth.getSession()`
* you just call this `await getSession()`
*/
event.locals.getSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
return session;
};
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range';
}
});
};
`` |
Where exactly does the error appear? You are fetching some data from a sveltekit api endpoint, are you using supabase in there or is this fetch call not even executed? |
@david-plugge It's not using Supabase in that API endpoint, and it does execute, however it breaks when it's trying to return the response from the API endpoint back to the load function: This is essentially what my API endpoint is doing: import type { RequestHandler } from './$types';
import { error, type Config } from '@sveltejs/kit';
export const config: Config = {
runtime: 'edge'
};
export const POST: RequestHandler = async ({ request, fetch }) => {
try {
const requestData = await request.json();
const response = await fetch('https://someendpoint', {
headers: {
Authorization: `Bearer ${SOME_API_KEY}`,
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify(requestData)
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: 200,
statusText: 'Itinerary generated successfully.'
});
} catch (err) {
console.error(err);
throw error(500, 'An error occurred');
}
}; This is the error I get:
|
ouhh thats interesting. So what happens if you don´t use fetch from load but rather the global fetch with the full url (http://localhost:5173/api/itineraries/generate) And also, can you check if the api returns a |
Thinking about this a bit more it may also be caused by the request scoped supabase instance. This is getting kind of tricky.... |
Oh this is interesting, when I use a global fetch function with the full URL the set cookie error goes away. Here's the headers from the response from the API endpoint using the global fetch and full URL: |
Yeah so fetch from load directly forwards the request to the api endpoint if you pass in a url without an origin, completely skipping the http layer. It also tries to emulate the clients behaviour by sending the cookies along. Could you try passing |
@david-plugge oh that worked! Here's the headers: |
Glad to hear! |
Hi @david-plugge - thanks for your help again with this, just wanted to raise this as I realized there are still issues with the current approach of omitting credentials. For example, if I'm protecting my API routes following the guide here or here: And I have this in my hooks.server.ts // protect API requests to all routes that start with /api
if (event.url.pathname.startsWith('/api')) {
const session = await event.locals.getSession();
if (!session) {
// the user is not signed in
throw error(401, { message: 'Unauthorized' });
}
} The Do you think there would be any workarounds in the meantime? And to clarify regarding a longer-term plan, would this actually be an issue the auth-helpers maintainers would be figuring out a patch for or is this something that will require the SvelteKit team to address? |
I wonder if there is an actual benefit of using this approach (streaming). It might be easier to just load the lazy data on the client so you don´t have to worry about all this trouble. But if you still prefer streaming data you would need to create a custom supabase client, similar to export const handle: Handle = ({ event, resolve }) => {
if (event.request.headers.has('Authorization')) {
const [_, token] = event.request.headers.get('Authorization').split(' ');
if (token) {
event.locals.supabase = createLazyClient(token);
}
}
if (!event.locals.supabase) {
// default supabase auth helpers implementation
}
event.locals.getSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
return session;
};
return resolve(event, {
/**
* There´s an issue with `filterSerializedResponseHeaders` not working when using `sequence`
*
* https://github.com/sveltejs/kit/issues/8061
*/
filterSerializedResponseHeaders(name) {
return name === 'content-range';
}
});
} This client is not able to modify the actual session at all! |
Hi, just to add to this, I'm also getting same error. I'm unable to pin point where it's coming from from exactly. Cannot reproduce it yet either. This is the error message:
I'll be trying to consistently reproduce. It would be super helpful If you could point me to some direction. Thanks! |
Hi, In case this helps anyone else out that lands here look for a solution to this error, I was also having this issue but when trying to log a user in. It turned out to be I had forgotten to await .signInWithPassword() so it was trying to set the cookie after the response had been sent. Oops |
I am able to reproduce this error with the example "Sveltekit Email password" with no changes. It is easier to reproduce when setting "JWT expiry limit" to 30 seconds. Then navigate to the base path ("/") via the link in the layout 30 seconds after you logged in. From my testing it seems to be some issue with using load function in +page.ts file and having actions in +page.server.ts. When removing the +page.server.ts the error doesn't appear. https://github.com/supabase/auth-helpers/tree/main/examples/sveltekit-email-password When moving all load functions inside +page.server.ts and removing +page.ts, then it works. |
@tobiassern can you reproduce it with an expiry limit of 90 seconds? The server client always refreshes the session when it´s valid for less than 60 seconds by default. |
@david-plugge Thanks for the information, didn't know that. But I can still reproduce it with an expiry limit of 90 seconds. Although if I move the load function to page.server.ts and removes page.ts it works as intended. |
I have created a minimal, reproducible example for this error and carefully documented it in the README file. Hopefully, it serves useful in resolving the issue, as it's currently blocking our production release. |
Thank you very much for the reproduction @rudgalvis ! I´ve been playing around with it and it seems like the supabase client isn´t actually used when navigating between +let called = false;
event.locals.getSession = async () => {
+ called = true;
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
return session;
};
+if (!called) {
+ await event.locals.getSession();
+} This makes sure I´m not sure how we should actually make this work properly without this weird workaround, it feels like there is something missing to sveltekit like a simple boolean to check if the headers are already sent. I´ll think about it over the weekend but for the meantime the code snippet above should help you getting rid of the error. |
This didn't work, instance suffered from same problem and crashed. Using the exact code from https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit |
This seems to still be an issue with versions:
Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"Error: Cannot use |
I've the same happening, when a user opens a url in a blank browser window and their sessions token needs a refresh, reproduced it by setting the refresh timeout to a low number -> got a sessions-> closed the window -> waited -> navigated to the url in blank window after the set timeout was expired. I've been able to resolve hard crashes, on my occurrence of this error, by adding
This is far from ideal, the needed cookie actions do not get applied. My interpretation of the error, in my setup, is that See the sveltekit source where the
I have to add that my async/await skills are mediocre at best, my analysis might be way off. The packages I used:
|
I came to a new 'solution', to make sure the The code I added to
The whole code in
For now I'm logging any errors that get passed back by Before settling on awaiting
Edit, this is a bit off-topic: |
I'm facing this error when trying to make a call to I tried awaiting for the session as some suggested here, but the result seems to be the same. SvelteKit is at Does anyone know if this is a thing with the SSR package? Should I switch? UPD: yep, looked at the imports of others here, looks like it's still a thing with the ssr too. |
I'm still running into this issue when I updated to the SSR package. Here's my versions: |
Bug report
Describe the bug
When using SvelteKit's new streaming promises feature, it seems to run into an issue setting cookies with the logic that runs in server hooks mentioned here.
Here is the error:
If I comment out all the logic in my hooks file, the error seems to go away, and the data from the streamed promise will return correctly.
Here is my
+page.server.ts
load function that is calling the API endpoint:The API endpoint itself returns fine, however when trying to utilize streaming promises, it seems to break. A regular top-level awaited promise will work as well. But with a streaming promise, it will run into the cookie error.
To Reproduce
Steps to reproduce the behavior, please provide code snippets or a repository:
hooks.server.ts
as instructed according to Supabase docsExpected behavior
The load function should unwrap the streaming promise from the API endpoint and return the data.
Screenshots
If applicable, add screenshots to help explain your problem.
System information
The text was updated successfully, but these errors were encountered: