Skip to content

Commit 2716622

Browse files
wobsorianoLekoArts
andauthored
fix(clerk-js,astro,nuxt,tanstack-react-start,remix,react-router): Introduce helper function to prevent infinite handshake in Netlify (#5656)
Co-authored-by: Lennart <[email protected]>
1 parent fe61d98 commit 2716622

File tree

15 files changed

+169
-49
lines changed

15 files changed

+169
-49
lines changed

.changeset/plenty-hounds-sort.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@clerk/astro": patch
3+
"@clerk/clerk-js": patch
4+
"@clerk/nuxt": patch
5+
"@clerk/react-router": patch
6+
"@clerk/remix": patch
7+
"@clerk/shared": patch
8+
"@clerk/tanstack-react-start": patch
9+
---
10+
11+
Fix handshake redirect loop in applications deployed to Netlify with a Clerk development instance.

packages/astro/src/integration/create-integration.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
125125
'page',
126126
`
127127
${command === 'dev' ? `console.log("${packageName}","Initialize Clerk: page")` : ''}
128-
import { removeNetlifyCacheBustParam, runInjectionScript, swapDocument } from "${buildImportPath}";
129-
130-
// Fix an issue with infinite redirect in Netlify and Clerk dev instance
131-
removeNetlifyCacheBustParam();
128+
import { runInjectionScript, swapDocument } from "${buildImportPath}";
132129
133130
// Taken from https://github.com/withastro/astro/blob/e10b03e88c22592fbb42d7245b65c4f486ab736d/packages/astro/src/transitions/router.ts#L39.
134131
// Importing it directly from astro:transitions/client breaks custom client-side routing

packages/astro/src/internal/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,3 @@ export { runInjectionScript };
1414

1515
export { generateSafeId } from './utils/generateSafeId';
1616
export { swapDocument } from './swap-document';
17-
export { NETLIFY_CACHE_BUST_PARAM, removeNetlifyCacheBustParam } from './remove-query-param';

packages/astro/src/internal/remove-query-param.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

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

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { AuthObject, ClerkClient } from '@clerk/backend';
22
import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal';
33
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
4-
import { isDevelopmentFromPublishableKey, isDevelopmentFromSecretKey } from '@clerk/shared/keys';
4+
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
5+
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
56
import { isHttpOrHttps } from '@clerk/shared/proxy';
67
import { handleValueOrFn } from '@clerk/shared/utils';
78
import type { APIContext } from 'astro';
89

910
import { authAsyncStorage } from '#async-local-storage';
1011

11-
import { NETLIFY_CACHE_BUST_PARAM } from '../internal';
1212
import { buildClerkHotloadScript } from './build-clerk-hotload-script';
1313
import { clerkClient } from './clerk-client';
1414
import { createCurrentUser } from './current-user';
@@ -74,7 +74,11 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
7474

7575
const locationHeader = requestState.headers.get(constants.Headers.Location);
7676
if (locationHeader) {
77-
handleNetlifyCacheInDevInstance(locationHeader, requestState);
77+
handleNetlifyCacheInDevInstance({
78+
locationHeader,
79+
requestStateHeaders: requestState.headers,
80+
publishableKey: requestState.publishableKey,
81+
});
7882

7983
const res = new Response(null, { status: 307, headers: requestState.headers });
8084
return decorateResponseWithObservabilityHeaders(res, requestState);
@@ -234,25 +238,6 @@ Check if signInUrl is missing from your configuration or if it is not an absolut
234238
PUBLIC_CLERK_SIGN_IN_URL='SOME_URL'
235239
PUBLIC_CLERK_IS_SATELLITE='true'`;
236240

237-
/**
238-
* Prevents infinite redirects in Netlify's functions
239-
* by adding a cache bust parameter to the original redirect URL. This ensures Netlify
240-
* doesn't serve a cached response during the authentication flow.
241-
*/
242-
function handleNetlifyCacheInDevInstance(locationHeader: string, requestState: RequestState) {
243-
// Only run on Netlify environment and Clerk development instance
244-
// eslint-disable-next-line turbo/no-undeclared-env-vars
245-
if (import.meta.env.NETLIFY && isDevelopmentFromPublishableKey(requestState.publishableKey)) {
246-
const hasHandshakeQueryParam = locationHeader.includes('__clerk_handshake');
247-
// If location header is the original URL before the handshake redirects, add cache bust param
248-
if (!hasHandshakeQueryParam) {
249-
const url = new URL(locationHeader);
250-
url.searchParams.append(NETLIFY_CACHE_BUST_PARAM, Date.now().toString());
251-
requestState.headers.set('Location', url.toString());
252-
}
253-
}
254-
}
255-
256241
function decorateAstroLocal(clerkRequest: ClerkRequest, context: APIContext, requestState: RequestState) {
257242
const { reason, message, status, token } = requestState;
258243
context.locals.authToken = token;

packages/clerk-js/src/core/clerk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ClerkRuntimeError, EmailLinkErrorCodeStatus, is4xxError, isClerkAPIResp
55
import { parsePublishableKey } from '@clerk/shared/keys';
66
import { LocalStorageBroadcastChannel } from '@clerk/shared/localStorageBroadcastChannel';
77
import { logger } from '@clerk/shared/logger';
8+
import { CLERK_NETLIFY_CACHE_BUST_PARAM } from '@clerk/shared/netlifyCacheHandler';
89
import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy';
910
import {
1011
eventPrebuiltComponentMounted,
@@ -2622,6 +2623,7 @@ export class Clerk implements ClerkInterface {
26222623
#clearClerkQueryParams = () => {
26232624
try {
26242625
removeClerkQueryParam(CLERK_SYNCED);
2626+
removeClerkQueryParam(CLERK_NETLIFY_CACHE_BUST_PARAM);
26252627
// @nikos: we're looking into dropping this param completely
26262628
// in the meantime, we're removing it here to keep the URL clean
26272629
removeClerkQueryParam(CLERK_SUFFIXED_COOKIES);

packages/clerk-js/src/utils/getClerkQueryParam.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { EmailLinkErrorCodeStatus } from '@clerk/shared/error';
2+
import { CLERK_NETLIFY_CACHE_BUST_PARAM } from '@clerk/shared/netlifyCacheHandler';
23

34
import { CLERK_SATELLITE_URL, CLERK_SUFFIXED_COOKIES, CLERK_SYNCED } from '../core/constants';
45

@@ -10,6 +11,7 @@ const _ClerkQueryParams = [
1011
'__clerk_modal_state',
1112
'__clerk_handshake',
1213
'__clerk_help',
14+
CLERK_NETLIFY_CACHE_BUST_PARAM,
1315
CLERK_SYNCED,
1416
CLERK_SATELLITE_URL,
1517
CLERK_SUFFIXED_COOKIES,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AuthenticateRequestOptions } from '@clerk/backend/internal';
22
import { AuthStatus, constants } from '@clerk/backend/internal';
33
import { deprecated } from '@clerk/shared/deprecated';
4+
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
45
import type { EventHandler } from 'h3';
56
import { createError, eventHandler, setResponseHeader } from 'h3';
67

@@ -84,6 +85,11 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
8485

8586
const locationHeader = requestState.headers.get(constants.Headers.Location);
8687
if (locationHeader) {
88+
handleNetlifyCacheInDevInstance({
89+
locationHeader,
90+
requestStateHeaders: requestState.headers,
91+
publishableKey: requestState.publishableKey,
92+
});
8793
// Trigger a handshake redirect
8894
return new Response(null, { status: 307, headers: requestState.headers });
8995
}

packages/react-router/src/ssr/authenticateRequest.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createClerkClient } from '@clerk/backend';
22
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
3-
import { AuthStatus } from '@clerk/backend/internal';
3+
import { AuthStatus, constants } from '@clerk/backend/internal';
4+
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
45

56
import type { LoaderFunctionArgs } from './types';
67
import { patchRequest } from './utils';
@@ -33,8 +34,13 @@ export async function authenticateRequest(
3334
afterSignUpUrl,
3435
});
3536

36-
const hasLocationHeader = requestState.headers.get('location');
37-
if (hasLocationHeader) {
37+
const locationHeader = requestState.headers.get(constants.Headers.Location);
38+
if (locationHeader) {
39+
handleNetlifyCacheInDevInstance({
40+
locationHeader,
41+
requestStateHeaders: requestState.headers,
42+
publishableKey: requestState.publishableKey,
43+
});
3844
// triggering a handshake redirect
3945
// eslint-disable-next-line @typescript-eslint/only-throw-error
4046
throw new Response(null, { status: 307, headers: requestState.headers });

packages/remix/src/ssr/authenticateRequest.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createClerkClient } from '@clerk/backend';
22
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
3-
import { AuthStatus } from '@clerk/backend/internal';
3+
import { AuthStatus, constants } from '@clerk/backend/internal';
4+
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
45

56
import type { LoaderFunctionArgs } from './types';
67
import { patchRequest } from './utils';
@@ -33,8 +34,13 @@ export async function authenticateRequest(
3334
afterSignUpUrl,
3435
});
3536

36-
const hasLocationHeader = requestState.headers.get('location');
37-
if (hasLocationHeader) {
37+
const locationHeader = requestState.headers.get(constants.Headers.Location);
38+
if (locationHeader) {
39+
handleNetlifyCacheInDevInstance({
40+
locationHeader,
41+
requestStateHeaders: requestState.headers,
42+
publishableKey: requestState.publishableKey,
43+
});
3844
// triggering a handshake redirect
3945
// eslint-disable-next-line @typescript-eslint/only-throw-error
4046
throw new Response(null, { status: 307, headers: requestState.headers });

0 commit comments

Comments
 (0)