Skip to content
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

Open
chanmathew opened this issue Mar 7, 2023 · 27 comments
Assignees
Labels
bug Something isn't working

Comments

@chanmathew
Copy link

chanmathew commented Mar 7, 2023

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:

Error: Cannot use `cookies.set(...)` after the response has been generated
    at Object.event.cookies.set (/node_modules/@sveltejs/kit/src/runtime/server/respond.js:422:11)
    at fetch (/node_modules/@sveltejs/kit/src/runtime/server/fetch.js:150:21)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.eval [as fetch] (/node_modules/@sveltejs/kit/src/runtime/server/fetch.js:32:10)
    at async generateItinerary (/src/routes/(app)/trips/[tripId]/itinerary/+page.server.ts:7:22)

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:

export const load = (async ({ parent, fetch, locals, params }) => {
	const generateItinerary = async () => {
		const response = await fetch('/api/itineraries/generate', {
			method: 'POST',
			body: JSON.stringify({
				destination: 'Paris, France',
				days: 1,
				preferences: 'must-see attractions'
			})
		});

		const data = await response.json(); <<< This is where it breaks
		console.log('DATA: ', data); <<< This console log never happens due to the cookie error thrown

		return data;
	};

	return {
		lazy: {
			itineraries: generateItinerary()
		}
	};
}) satisfies PageServerLoad;

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:

  1. Setup hooks.server.ts as instructed according to Supabase docs
  2. Create an API endpoint
  3. Create a layout or page server load function to fetch from API endpoint
  4. The load function will fail to return the data from the API endpoint, and error appears server console

Expected 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

  • OS: macOS
  • Version of supabase-js: v2.10.0
  • Version of supabase-auth-helpers-sveltekit: v0.9.0
  • Version of Node.js: v16.17.0
  • Version of SvelteKit: v1.10.0
@chanmathew chanmathew added the bug Something isn't working label Mar 7, 2023
@david-plugge
Copy link
Collaborator

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.
Therefore, this does not only affect auth helpers, but all situations where you want to set a cookie this way.
To work around this limitation you can implement it in a way like this:

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;

@chanmathew
Copy link
Author

chanmathew commented Mar 10, 2023

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?

@david-plugge
Copy link
Collaborator

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.
Could you share your code? It´s easier to check for errors with an example.

@chanmathew
Copy link
Author

chanmathew commented Mar 10, 2023

@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';
		}
	});
};
``

@david-plugge
Copy link
Collaborator

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?

@chanmathew
Copy link
Author

chanmathew commented Mar 10, 2023

@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:

src/routes/(app)/trips/[tripId]/+layout.server.ts: Calling `event.fetch(...)` in a promise handler after `load(...)` has returned will not cause the function to re-run when the dependency is invalidated
/node_modules/@sveltejs/kit/src/runtime/server/respond.js:422
				throw new Error('Cannot use `cookies.set(...)` after the response has been generated');
				      ^

Error: Cannot use `cookies.set(...)` after the response has been generated
    at Object.event.cookies.set (/node_modules/@sveltejs/kit/src/runtime/server/respond.js:422:11)
    at fetch (/node_modules/@sveltejs/kit/src/runtime/server/fetch.js:150:21)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Object.eval [as fetch] (/node_modules/@sveltejs/kit/src/runtime/server/fetch.js:32:10)
    at async fetchItineraries (/src/routes/(app)/trips/[tripId]/+layout.server.ts:31:24)

@david-plugge
Copy link
Collaborator

david-plugge commented Mar 10, 2023

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 set-cookie header? Sveltekit would try to merge them into the actual request which could result in the error beeing thrown.

@david-plugge
Copy link
Collaborator

Thinking about this a bit more it may also be caused by the request scoped supabase instance. This is getting kind of tricky....

@chanmathew
Copy link
Author

chanmathew commented Mar 10, 2023

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 set-cookie header? Sveltekit would try to merge them into the actual request which could result in the error beeing thrown.

Oh this is interesting, when I use a global fetch function with the full URL the set cookie error goes away.
And, using the load fetch function, but passing in the full URL, the set cookie error also goes away.
But when I use the load fetch function, and only pass in the relative URL, that's when it breaks and I see the set cookie error.

Here's the headers from the response from the API endpoint using the global fetch and full URL:
CleanShot 2023-03-10 at 16 15 22@2x

@david-plugge
Copy link
Collaborator

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 credentials: 'omit' to the load fetch? Not sure if they implemented that though.

@chanmathew
Copy link
Author

@david-plugge oh that worked! Here's the headers:
CleanShot 2023-03-10 at 16 22 20@2x

@david-plugge
Copy link
Collaborator

Glad to hear!

@chanmathew
Copy link
Author

chanmathew commented Mar 14, 2023

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 session in that case is always null, and therefore will not be able to hit the endpoint since it'll throw a 401.

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?

@david-plugge
Copy link
Collaborator

david-plugge commented Mar 14, 2023

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.
(In case you didnt know, streaming promises require js on the client anyway)

But if you still prefer streaming data you would need to create a custom supabase client, similar to createLazyClient if the request has a Authorization header. This isn´t tested at all, just written of the top of my head.

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!

@RokasRudgalvis
Copy link

RokasRudgalvis commented Mar 30, 2023

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:

throw new Error('Cannot use `cookies.set(...)` after the response has been generated');
                                      ^

Error: Cannot use `cookies.set(...)` after the response has been generated
    at event.cookies.set (/node_modules/@sveltejs/kit/src/runtime/server/respond.js:422:11)
    at Object.setItem (file:///Users/r/WebstormProjects/monolith/node_modules/@supabase/auth-helpers-sveltekit/dist/index.js:152:15)
    at /Users/r/WebstormProjects/monolith/node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:128:19
    at Generator.next (<anonymous>)
    at /Users/r/WebstormProjects/monolith/node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:31:71
    at new Promise (<anonymous>)
    at __awaiter (/Users/r/WebstormProjects/monolith/node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:27:12)
    at setItemAsync (/Users/r/WebstormProjects/monolith/node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:127:46)
    at SupabaseAuthClient._persistSession (/Users/r/WebstormProjects/monolith/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:957:43)
    at SupabaseAuthClient.<anonymous> (/Users/r/WebstormProjects/monolith/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:952:28)

I'll be trying to consistently reproduce. It would be super helpful If you could point me to some direction. Thanks!

@JowinJ
Copy link

JowinJ commented Apr 1, 2023

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

@tobiassern
Copy link

tobiassern commented Apr 18, 2023

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.

@david-plugge
Copy link
Collaborator

@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.

@tobiassern
Copy link

@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.

@rudgalvis
Copy link

Minimal reproduction

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.

@david-plugge david-plugge self-assigned this Aug 25, 2023
@david-plugge
Copy link
Collaborator

david-plugge commented Aug 25, 2023

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 /posts and /auth. SvelteKit makes a request to the server and since there is a shared layout load function at the root that does all the work the response is basicly empty (just some sveltekit internal stuff telling the client it has all data).
So what happens is that the client will be created in the handle hook and lazily calls getSession some time in the future (no idea why tbh). I managed to create a workaround:

+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 getSession is called before the response is created (the headers are sent).

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.

@mennankara
Copy link

+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 getSession is called before the response is created (the headers are sent).

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

@leerobert
Copy link

leerobert commented Oct 9, 2023

This seems to still be an issue with versions:

		"@supabase/auth-helpers-sveltekit": "^0.10.3",
		"@supabase/supabase-js": "^2.38.0",
		"@sveltejs/adapter-auto": "^2.0.0",
		"@sveltejs/kit": "^1.20.4",

Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"Error: Cannot use cookies.set(...) after the response has been generated","reason":{"errorType":"Error","errorMessage":"Cannot use cookies.set(...) after the response has been generated","stack":["Error: Cannot use cookies.set(...) after the response has been generated"," at event2.cookies.set (file:///var/task/vercel/path0/.svelte-kit/output/server/index.js:3189:15)"," at SvelteKitServerAuthStorageAdapter.setCookie (file:///var/task/vercel/path0/node_modules/.pnpm/@supabase+auth-helpers-sveltekit@0.10.3_@supabase+supabase-js@2.32.0_@sveltejs+kit@1.20.4/node_modules/@supabase/auth-helpers-sveltekit/dist/index.js:80:24)"," at SvelteKitServerAuthStorageAdapter.setItem (/var/task/vercel/path0/node_modules/.pnpm/@supabase+auth-helpers-shared@0.5.0_@supabase+supabase-js@2.32.0/node_modules/@supabase/auth-helpers-shared/dist/index.js:281:10)"," at setItemAsync (/var/task/vercel/path0/node_modules/.pnpm/@supabase+gotrue-js@2.54.0/node_modules/@supabase/gotrue-js/dist/main/lib/helpers.js:129:19)"," at SupabaseAuthClient._persistSession (/var/task/vercel/path0/node_modules/.pnpm/@supabase+gotrue-js@2.54.0/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:1357:43)"," at SupabaseAuthClient._saveSession (/var/task/vercel/path0/node_modules/.pnpm/@supabase+gotrue-js@2.54.0/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:1353:20)"," at SupabaseAuthClient._callRefreshToken (/var/task/vercel/path0/node_modules/.pnpm/@supabase+gotrue-js@2.54.0/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:1298:24)"," at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"," at async SupabaseAuthClient.__loadSession (/var/task/vercel/path0/node_modules/.pnpm/@supabase+gotrue-js@2.54.0/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:755:40)"," at async SupabaseAuthClient._useSession (/var/task/vercel/path0/node_modules/.pnpm/@supabase+gotrue-js@2.54.0/node_modules/@supabase/gotrue-js/dist/main/GoTrueClient.js:715:28)"]},"promise":{},"stack":["Runtime.UnhandledPromiseRejection: Error: Cannot use cookies.set(...) after the response has been generated"," at process. (file:///var/runtime/index.mjs:1250:17)"," at process.emit (node:events:526:35)"," at emit (node:internal/process/promises:149:20)"," at processPromiseRejections (node:internal/process/promises:283:27)"," at process.processTicksAndRejections (node:internal/process/task_queues:96:32)"]}
Unknown application error occurred
Runtime.Unknown

@jordvisser
Copy link

jordvisser commented Nov 30, 2023

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 try-catch blocks to the event.cookies.set & event.cookies.delete functions with the following in the hooks.server.ts file:

event.locals.supabase = createServerClient(
    PUBLIC_SUPABASE_URL,
    PUBLIC_SUPABASE_ANON_KEY,
    {
        cookies: {
            get: (key) => event.cookies.get(key),
            set: (key, value, options) => {
                try {
                    event.cookies.set(key, value, options);   
                } catch (err) {
                    console.error({err})
                }
            },
            remove: (key, options) => {
                try {
                    event.cookies.delete(key, options);  
                } catch (err) {
                    console.error({err})
                }
            }
        }
    }
);

This is far from ideal, the needed cookie actions do not get applied.

My interpretation of the error, in my setup, is that createServerClient is using an async method to set the cookies, but these resolve/execute after sveltekit already passed on the event to the resolve function.

See the sveltekit source where the event.cookie.set function gets overloaded: sveltejs/kit/.../runtime/server/respond.js
This is the only occurrence I could find where this error text is used in all of svelte's repositories

......
} finally {
    event.cookies.set = () => {
        throw new Error('Cannot use `cookies.set(...)` after the response has been generated');
    };

    event.setHeaders = () => {
        throw new Error('Cannot use `setHeaders(...)` after the response has been generated');
    };
}
......

I have to add that my async/await skills are mediocre at best, my analysis might be way off.

The packages I used:

"@sveltejs/kit": "^1.20.4",
"svelte": "^4.2.7",
"@supabase/ssr": "^0.0.10",
"@supabase/supabase-js": "^2.38.5",

@jordvisser
Copy link

jordvisser commented Nov 30, 2023

I came to a new 'solution', to make sure the createServerClient is done with all the setting/removing of cookies I await a call to ...supabase.auth.getSession() before returning the resolve function in the hooks.server.ts handle.
I removed the try-catch blocks as they might hide other severe errors.

The code I added to hooks.server.ts:

const { error: getSessionError } = await event.locals.supabase.auth.getSession()
if (getSessionError) {
    console.error(getSessionError)
}

The whole code in hooks.server.ts as it is right now:

import {
    PUBLIC_SUPABASE_ANON_KEY,
    PUBLIC_SUPABASE_URL
} from '$env/static/public';
import { createServerClient } from '@supabase/ssr';
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
    event.locals.supabase = createServerClient(
        PUBLIC_SUPABASE_URL,
        PUBLIC_SUPABASE_ANON_KEY,
        {
            cookies: {
                get: (key) => event.cookies.get(key),
                set: (key, value, options) => {
                    event.cookies.set(key, value, options);
                },
                remove: (key, options) => {
                    event.cookies.delete(key, options);
                }
            }
        }
    );

    event.locals.getSession = async () => {
        const {
            data: { session }
        } = await event.locals.supabase.auth.getSession();
        return session;
    };
    

    const { error: getSessionError } = await event.locals.supabase.auth.getSession()
    if (getSessionError) {
        console.error(getSessionError)
    }

    return resolve(event, {
        filterSerializedResponseHeaders(name) {
            return name === 'content-range';
        }
    });
};

For now I'm logging any errors that get passed back by getSession(), is that a good practice?

Before settling on awaiting getSession() I tried awaiting ...supabase.auth.initializePromise, but this did not prevent the error from occurring.
I would be nice to have something that indicates if createServerClient is still busy, with which we can await or do something else to make sure no cookies/headers get set after the event is passed on to resolve.
The code snippet I used to try to use the initializePromise

const {error: supabaseAuthInitializeError} = await event.locals.supabase.auth.initializePromise;
if (supabaseAuthInitializeError) {
    console.error(supabaseAuthInitializeError);
}

Edit, this is a bit off-topic:
I tried to understand the error states of getSession() a bit better, as to only log errors if they are not an instanceof AuthError, to keep unnecessary logging to a minimum.
It seems that next to setting the current error in the { error } return of getSession(), the supabase.auth part is emitting errors to the console as well.
What is the proper way to setup the supabase.auth logging behaviour? I could not arrive at the specific page in the docs by searching for logging/debug(ging).

@igorlanko
Copy link

igorlanko commented Dec 18, 2023

I'm facing this error when trying to make a call to /api/endpoint/+server.ts from a companion Chrome extension. My hook intercepts the request and attaches the bearer token to initiate the supabase client with. Other than that the code is the same as from the supabase docs.

I tried awaiting for the session as some suggested here, but the result seems to be the same. SvelteKit is at 1.29.1.

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.

@chanmathew
Copy link
Author

I'm still running into this issue when I updated to the SSR package. Here's my versions:
"@sveltejs/kit": "^2.0.0",
"svelte": "5.0.0-next.55",
"supabase": "^1.145.4",
"@supabase/ssr": "^0.1.0",
"@supabase/supabase-js": "^2.39.2",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

10 participants