Skip to content

Commit 87fed8f

Browse files
committed
fix(shared): Include suffixed cookie names in Netlify-Vary header
Clerk uses suffixed cookies (e.g. __client_uat_AbC12345) by default for newer instances. The Netlify-Vary header now includes both unsuffixed and suffixed cookie names computed from the publishable key, ensuring CDN cache isolation works for all Clerk instances.
1 parent c53abdf commit 87fed8f

7 files changed

Lines changed: 57 additions & 34 deletions

File tree

packages/astro/src/server/clerk-middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
121121
createAuthenticateRequestOptions(clerkRequest, keylessOptions, context),
122122
);
123123

124-
handleNetlifyCacheHeaders(requestState);
124+
await handleNetlifyCacheHeaders(requestState);
125125

126126
const locationHeader = requestState.headers.get(constants.Headers.Location);
127127
if (locationHeader) {

packages/nextjs/src/server/clerkMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
221221
reason: requestState.reason,
222222
}));
223223

224-
handleNetlifyCacheHeaders(requestState);
224+
await handleNetlifyCacheHeaders(requestState);
225225

226226
const locationHeader = requestState.headers.get(constants.Headers.Location);
227227
if (locationHeader) {

packages/nuxt/src/runtime/server/clerkMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
8787
acceptsToken: 'any',
8888
});
8989

90-
handleNetlifyCacheHeaders(requestState);
90+
await handleNetlifyCacheHeaders(requestState);
9191

9292
const locationHeader = requestState.headers.get(constants.Headers.Location);
9393
if (locationHeader) {

packages/react-router/src/server/clerkMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareFun
8888
__keylessApiKeysUrl,
8989
});
9090

91-
handleNetlifyCacheHeaders(requestState);
91+
await handleNetlifyCacheHeaders(requestState);
9292

9393
const locationHeader = requestState.headers.get(constants.Headers.Location);
9494
if (locationHeader) {

packages/shared/src/__tests__/netlifyCacheHandler.spec.ts

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable turbo/no-undeclared-env-vars */
22
import { beforeEach, describe, expect, it } from 'vitest';
33

4+
import { getCookieSuffix } from '../keys';
45
import { CLERK_NETLIFY_CACHE_BUST_PARAM, handleNetlifyCacheHeaders } from '../netlifyCacheHandler';
56

67
const mockDevPublishableKey = 'pk_test_YW55LW9mZabcZS1wYWdlX3BhZ2VfcG9pbnRlci1pZF90ZXN0XzE';
@@ -14,86 +15,98 @@ describe('handleNetlifyCacheHeaders', () => {
1415
});
1516

1617
describe('Netlify-Vary header', () => {
17-
it('should set Netlify-Vary header when on Netlify', () => {
18+
it('should set Netlify-Vary header with unsuffixed and suffixed cookie names when on Netlify', async () => {
1819
process.env.NETLIFY = 'true';
1920

2021
const headers = new Headers();
21-
handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
22+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
2223

23-
expect(headers.get('Netlify-Vary')).toBe('cookie=__client_uat,cookie=__session');
24+
const suffix = await getCookieSuffix(mockProdPublishableKey);
25+
expect(headers.get('Netlify-Vary')).toBe(
26+
`cookie=__client_uat,cookie=__session,cookie=__client_uat_${suffix},cookie=__session_${suffix}`,
27+
);
2428
});
2529

26-
it('should not set Netlify-Vary header when not on Netlify', () => {
30+
it('should not set Netlify-Vary header when not on Netlify', async () => {
2731
const headers = new Headers();
28-
handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
32+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
2933

3034
expect(headers.get('Netlify-Vary')).toBeNull();
3135
});
3236

33-
it('should detect Netlify via NETLIFY_FUNCTIONS_TOKEN', () => {
37+
it('should detect Netlify via NETLIFY_FUNCTIONS_TOKEN', async () => {
3438
process.env.NETLIFY_FUNCTIONS_TOKEN = 'some-token';
3539

3640
const headers = new Headers();
37-
handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
41+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
3842

39-
expect(headers.get('Netlify-Vary')).toBe('cookie=__client_uat,cookie=__session');
43+
expect(headers.get('Netlify-Vary')).toContain('cookie=__client_uat');
4044
});
4145

42-
it('should detect Netlify via URL ending with netlify.app', () => {
46+
it('should detect Netlify via URL ending with netlify.app', async () => {
4347
process.env.URL = 'https://example.netlify.app';
4448

4549
const headers = new Headers();
46-
handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
50+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
51+
52+
expect(headers.get('Netlify-Vary')).toContain('cookie=__client_uat');
53+
});
54+
55+
it('should fall back to unsuffixed cookies only when publishableKey is empty', async () => {
56+
process.env.NETLIFY = 'true';
57+
58+
const headers = new Headers();
59+
await handleNetlifyCacheHeaders({ headers, publishableKey: '' });
4760

4861
expect(headers.get('Netlify-Vary')).toBe('cookie=__client_uat,cookie=__session');
4962
});
5063
});
5164

5265
describe('cache bust parameter (dev instances)', () => {
53-
it('should add cache bust parameter when on Netlify and in development with Location header', () => {
66+
it('should add cache bust parameter when on Netlify and in development with Location header', async () => {
5467
process.env.NETLIFY = 'true';
5568

5669
const headers = new Headers({ Location: 'https://example.netlify.app' });
57-
handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
70+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
5871

5972
const locationUrl = new URL(headers.get('Location') || '');
6073
expect(locationUrl.searchParams.has(CLERK_NETLIFY_CACHE_BUST_PARAM)).toBe(true);
6174
});
6275

63-
it('should not add cache bust parameter for production instances', () => {
76+
it('should not add cache bust parameter for production instances', async () => {
6477
process.env.NETLIFY = 'true';
6578

6679
const headers = new Headers({ Location: 'https://example.netlify.app' });
67-
handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
80+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockProdPublishableKey });
6881

6982
const locationUrl = new URL(headers.get('Location') || '');
7083
expect(locationUrl.searchParams.has(CLERK_NETLIFY_CACHE_BUST_PARAM)).toBe(false);
7184
});
7285

73-
it('should not modify the Location header if it has the handshake param', () => {
86+
it('should not modify the Location header if it has the handshake param', async () => {
7487
process.env.NETLIFY = 'true';
7588

7689
const locationValue = 'https://example.netlify.app/redirect?__clerk_handshake=';
7790
const headers = new Headers({ Location: locationValue });
78-
handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
91+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
7992

8093
expect(headers.get('Location')).toBe(locationValue);
8194
});
8295

83-
it('should not modify the Location header if not on Netlify', () => {
96+
it('should not modify the Location header if not on Netlify', async () => {
8497
const headers = new Headers({ Location: 'https://example.com' });
85-
handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
98+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
8699

87100
expect(headers.get('Location')).toBe('https://example.com');
88101
});
89102

90-
it('should ignore the URL environment variable if it is not a string', () => {
103+
it('should ignore the URL environment variable if it is not a string', async () => {
91104
// @ts-expect-error - Random object
92105
process.env.URL = {};
93106
process.env.NETLIFY = 'true';
94107

95108
const headers = new Headers({ Location: 'https://example.netlify.app' });
96-
handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
109+
await handleNetlifyCacheHeaders({ headers, publishableKey: mockDevPublishableKey });
97110

98111
const locationUrl = new URL(headers.get('Location') || '');
99112
expect(locationUrl.searchParams.has(CLERK_NETLIFY_CACHE_BUST_PARAM)).toBe(true);

packages/shared/src/netlifyCacheHandler.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable turbo/no-undeclared-env-vars */
2-
import { isDevelopmentFromPublishableKey } from './keys';
2+
import { getCookieSuffix, isDevelopmentFromPublishableKey } from './keys';
33

44
/**
55
* Cache busting parameter for Netlify to prevent cached responses
@@ -32,23 +32,33 @@ function isNetlifyRuntime(): boolean {
3232
* Applies Netlify-specific cache headers to the request state.
3333
*
3434
* When running on Netlify, this function:
35-
* 1. Sets `Netlify-Vary: cookie=__client_uat,cookie=__session` to instruct Netlify's CDN
36-
* to create separate cache entries based on auth cookie values, preventing cached auth
37-
* state from bleeding across users/sessions.
35+
* 1. Sets `Netlify-Vary` with both unsuffixed and suffixed Clerk cookie names to instruct
36+
* Netlify's CDN to create separate cache entries based on auth cookie values, preventing
37+
* cached auth state from bleeding across users/sessions.
3838
* 2. For development instances with a redirect (Location header), adds a cache-bust query
3939
* parameter to prevent Netlify from serving cached responses during the handshake flow.
4040
*
4141
* @internal
4242
*/
43-
export function handleNetlifyCacheHeaders(requestState: { headers: Headers; publishableKey: string }): void {
43+
export async function handleNetlifyCacheHeaders(requestState: {
44+
headers: Headers;
45+
publishableKey: string;
46+
}): Promise<void> {
4447
if (!isNetlifyRuntime()) {
4548
return;
4649
}
4750

4851
const { headers, publishableKey } = requestState;
4952

50-
// Tell Netlify CDN to vary cache by auth cookie values (all instances, dev + prod)
51-
headers.set('Netlify-Vary', 'cookie=__client_uat,cookie=__session');
53+
// Tell Netlify CDN to vary cache by auth cookie values (all instances, dev + prod).
54+
// Include both unsuffixed and suffixed cookie names since Clerk uses suffixed cookies
55+
// by default for newer instances (e.g. __client_uat_AbC12345).
56+
const cookieNames = ['__client_uat', '__session'];
57+
if (publishableKey) {
58+
const suffix = await getCookieSuffix(publishableKey);
59+
cookieNames.push(`__client_uat_${suffix}`, `__session_${suffix}`);
60+
}
61+
headers.set('Netlify-Vary', cookieNames.map(name => `cookie=${name}`).join(','));
5262

5363
// Add cache-bust param to redirect URL for dev instances to prevent cached redirects
5464
const locationHeader = headers.get('Location');
@@ -66,7 +76,7 @@ export function handleNetlifyCacheHeaders(requestState: { headers: Headers; publ
6676
* @deprecated Use `handleNetlifyCacheHeaders` instead.
6777
* @internal
6878
*/
69-
export function handleNetlifyCacheInDevInstance({
79+
export async function handleNetlifyCacheInDevInstance({
7080
locationHeader: _locationHeader,
7181
requestStateHeaders,
7282
publishableKey,
@@ -75,5 +85,5 @@ export function handleNetlifyCacheInDevInstance({
7585
requestStateHeaders: Headers;
7686
publishableKey: string;
7787
}) {
78-
handleNetlifyCacheHeaders({ headers: requestStateHeaders, publishableKey });
88+
await handleNetlifyCacheHeaders({ headers: requestStateHeaders, publishableKey });
7989
}

packages/tanstack-react-start/src/server/clerkMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const clerkMiddleware = (
4848
acceptsToken: 'any',
4949
});
5050

51-
handleNetlifyCacheHeaders(requestState);
51+
await handleNetlifyCacheHeaders(requestState);
5252

5353
const locationHeader = requestState.headers.get(constants.Headers.Location);
5454
if (locationHeader) {

0 commit comments

Comments
 (0)