Skip to content

Commit db513c2

Browse files
committed
feat: fix ssr cookie handling in all edge cases
1 parent 627c57c commit db513c2

File tree

3 files changed

+198
-63
lines changed

3 files changed

+198
-63
lines changed

packages/ssr/src/createBrowserClient.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function createBrowserClient<
6262
await deleteChunks(
6363
key,
6464
async (chunkName) => {
65-
if (typeof cookies.get === 'function') {
65+
if ('get' in cookies && typeof cookies.get === 'function') {
6666
return await cookies.get(chunkName);
6767
}
6868
if (isBrowser()) {
@@ -71,7 +71,7 @@ export function createBrowserClient<
7171
}
7272
},
7373
async (chunkName) => {
74-
if (typeof cookies.remove === 'function') {
74+
if ('remove' in cookies.remove && typeof cookies.remove === 'function') {
7575
await cookies.remove(chunkName, {
7676
...DEFAULT_COOKIE_OPTIONS,
7777
...cookieOptions,

packages/ssr/src/createServerClient.ts

+178-56
Original file line numberDiff line numberDiff line change
@@ -46,25 +46,109 @@ export function createServerClient<
4646
};
4747
}
4848

49-
const deleteAllChunks = async (key: string) => {
50-
await deleteChunks(
51-
key,
52-
async (chunkName) => {
53-
if (typeof cookies.get === 'function') {
54-
return await cookies.get(chunkName);
49+
let storage: any;
50+
51+
if ('get' in cookies) {
52+
const deleteAllChunks = async (key: string) => {
53+
await deleteChunks(
54+
key,
55+
async (chunkName) => {
56+
if (typeof cookies.get === 'function') {
57+
return await cookies.get(chunkName);
58+
}
59+
},
60+
async (chunkName) => {
61+
if (typeof cookies.remove === 'function') {
62+
return await cookies.remove(chunkName, {
63+
...DEFAULT_COOKIE_OPTIONS,
64+
...cookieOptions,
65+
maxAge: 0
66+
});
67+
}
5568
}
69+
);
70+
};
71+
72+
storage = {
73+
// to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted
74+
isServer: true,
75+
getItem: async (key: string) => {
76+
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
77+
if (typeof cookies.get === 'function') {
78+
return await cookies.get(chunkName);
79+
}
80+
});
81+
return chunkedCookie;
5682
},
57-
async (chunkName) => {
58-
if (typeof cookies.remove === 'function') {
59-
return await cookies.remove(chunkName, {
60-
...DEFAULT_COOKIE_OPTIONS,
61-
...cookieOptions,
62-
maxAge: 0
63-
});
83+
setItem: async (key: string, value: string) => {
84+
if (typeof cookies.set === 'function') {
85+
// first delete all chunks so that there would be no overlap
86+
await deleteAllChunks(key);
87+
88+
const chunks = createChunks(key, value);
89+
90+
for (let i = 0; i < chunks.length; i += 1) {
91+
const chunk = chunks[i];
92+
93+
await cookies.set(chunk.name, chunk.value, {
94+
...DEFAULT_COOKIE_OPTIONS,
95+
...cookieOptions,
96+
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
97+
});
98+
}
99+
}
100+
},
101+
removeItem: async (key: string) => {
102+
if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') {
103+
console.log(
104+
'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createServerClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client'
105+
);
106+
return;
107+
}
108+
109+
await deleteAllChunks(key);
110+
}
111+
};
112+
}
113+
114+
const setItems: { [key: string]: string } = {};
115+
const removedItems: { [key: string]: boolean } = {};
116+
if ('getAll' in cookies) {
117+
storage = {
118+
// to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted
119+
isServer: true,
120+
getItem: async (key: string) => {
121+
if (typeof setItems[key] === 'string') {
122+
return setItems[key];
123+
}
124+
125+
if (removedItems[key]) {
126+
return null;
64127
}
128+
129+
const allCookies = await cookies.getAll();
130+
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
131+
const cookie = allCookies?.find(({ name }) => name === chunkName) || null;
132+
133+
if (!cookie) {
134+
return null;
135+
}
136+
137+
return cookie.value;
138+
});
139+
140+
return chunkedCookie;
141+
},
142+
setItem: async (key: string, value: string) => {
143+
setItems[key] = value;
144+
delete removedItems[key];
145+
},
146+
removeItem: async (key: string) => {
147+
delete setItems[key];
148+
removedItems[key] = true;
65149
}
66-
);
67-
};
150+
};
151+
}
68152

69153
const cookieClientOptions = {
70154
global: {
@@ -77,46 +161,7 @@ export function createServerClient<
77161
autoRefreshToken: isBrowser(),
78162
detectSessionInUrl: isBrowser(),
79163
persistSession: true,
80-
storage: {
81-
// to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted
82-
isServer: true,
83-
getItem: async (key: string) => {
84-
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
85-
if (typeof cookies.get === 'function') {
86-
return await cookies.get(chunkName);
87-
}
88-
});
89-
return chunkedCookie;
90-
},
91-
setItem: async (key: string, value: string) => {
92-
if (typeof cookies.set === 'function') {
93-
// first delete all chunks so that there would be no overlap
94-
await deleteAllChunks(key);
95-
96-
const chunks = createChunks(key, value);
97-
98-
for (let i = 0; i < chunks.length; i += 1) {
99-
const chunk = chunks[i];
100-
101-
await cookies.set(chunk.name, chunk.value, {
102-
...DEFAULT_COOKIE_OPTIONS,
103-
...cookieOptions,
104-
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
105-
});
106-
}
107-
}
108-
},
109-
removeItem: async (key: string) => {
110-
if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') {
111-
console.log(
112-
'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createServerClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client'
113-
);
114-
return;
115-
}
116-
117-
await deleteAllChunks(key);
118-
}
119-
}
164+
storage
120165
}
121166
};
122167

@@ -126,5 +171,82 @@ export function createServerClient<
126171
userDefinedClientOptions
127172
) as SupabaseClientOptions<SchemaName>;
128173

129-
return createClient<Database, SchemaName, Schema>(supabaseUrl, supabaseKey, clientOptions);
174+
const client = createClient<Database, SchemaName, Schema>(
175+
supabaseUrl,
176+
supabaseKey,
177+
clientOptions
178+
);
179+
180+
if ('getAll' in cookies) {
181+
client.auth.onAuthStateChange(async (event) => {
182+
if (event === 'TOKEN_REFRESHED' || event === 'USER_UPDATED' || event === 'SIGNED_OUT') {
183+
if (typeof cookies.setAll !== 'function') {
184+
console.log('You are holding it wrong!!!!!');
185+
}
186+
187+
const allCookies = await cookies.getAll();
188+
const cookieNames = allCookies?.map(({ name }) => name) || [];
189+
190+
const removeCookies = cookieNames.filter((name) => {
191+
if (removedItems[name]) {
192+
return true;
193+
}
194+
195+
const chunkLike = name.match(/^(.*)[.](0|[1-9][0-9]*)$/);
196+
if (chunkLike && removedItems[chunkLike[1]]) {
197+
return true;
198+
}
199+
200+
return false;
201+
});
202+
203+
const setCookies = Object.keys(setItems).flatMap((itemName) => {
204+
const removeExistingCookiesForItem = new Set(
205+
cookieNames.filter((name) => {
206+
if (name === itemName) {
207+
return true;
208+
}
209+
210+
const chunkLike = name.match(/^(.*)[.](0|[1-9][0-9]*)$/);
211+
if (chunkLike && chunkLike[1] === itemName) {
212+
return true;
213+
}
214+
215+
return false;
216+
})
217+
);
218+
219+
const chunks = createChunks(itemName, setItems[itemName]);
220+
221+
chunks.forEach((chunk) => {
222+
removeExistingCookiesForItem.delete(chunk.name);
223+
});
224+
225+
removeCookies.push(...removeExistingCookiesForItem);
226+
227+
return chunks;
228+
});
229+
230+
const removeCookieOptions = {
231+
...DEFAULT_COOKIE_OPTIONS,
232+
...cookieOptions,
233+
maxAge: 0
234+
};
235+
const setCookieOptions = {
236+
...DEFAULT_COOKIE_OPTIONS,
237+
...cookieOptions,
238+
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
239+
};
240+
241+
await cookies.setAll(
242+
[].concat(
243+
removeCookies.map((name) => ({ name, value: '', options: removeCookieOptions })),
244+
setCookies.map(({ name, value }) => ({ name, value, options: setCookieOptions }))
245+
)
246+
);
247+
}
248+
});
249+
}
250+
251+
return client;
130252
}

packages/ssr/src/types.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,21 @@ import type { CookieSerializeOptions } from 'cookie';
22

33
export type CookieOptions = Partial<CookieSerializeOptions>;
44
export type CookieOptionsWithName = { name?: string } & CookieOptions;
5-
export type CookieMethods = {
6-
get?: (key: string) => Promise<string | null | undefined> | string | null | undefined;
7-
set?: (key: string, value: string, options: CookieOptions) => Promise<void> | void;
8-
remove?: (key: string, options: CookieOptions) => Promise<void> | void;
9-
};
5+
export type CookieMethods =
6+
| {
7+
/** @deprecated Move to using `getAll` instead. */
8+
get: (key: string) => Promise<string | null | undefined> | string | null | undefined;
9+
/** @deprecated Move to using `setAll` instead. */
10+
set?: (key: string, value: string, options: CookieOptions) => Promise<void> | void;
11+
/** @deprecated Move to using `setAll` instead. */
12+
remove?: (key: string, options: CookieOptions) => Promise<void> | void;
13+
}
14+
| {
15+
getAll: () =>
16+
| Promise<{ name: string; value: string }[] | null>
17+
| { name: string; value: string }[]
18+
| null;
19+
setAll?: (
20+
cookies: { key: string; value: string; options: CookieOptions }[]
21+
) => Promise<void> | void;
22+
};

0 commit comments

Comments
 (0)