Skip to content

Commit 9b23a3e

Browse files
panteliselefwobsoriano
authored andcommitted
fix(nextjs): Fix infinite redirect loop when page does not exist (#5073)
1 parent 1f4a8f8 commit 9b23a3e

File tree

5 files changed

+49
-6
lines changed

5 files changed

+49
-6
lines changed

.changeset/twenty-doors-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/nextjs': patch
3+
---
4+
5+
Bug fix: On keyless avoid infinite redirect loop when page does not exist and application is attempting to sync state with middleware.

integration/tests/next-quickstart-keyless.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ test.describe('Keyless mode @quickstart', () => {
3737
await app.teardown();
3838
});
3939

40+
test('Navigates to non existed page (/_not-found) without a infinite redirect loop.', async ({ page, context }) => {
41+
const u = createTestUtils({ app, page, context });
42+
await u.page.goToAppHome();
43+
await u.page.waitForClerkJsLoaded();
44+
await u.po.expect.toBeSignedOut();
45+
46+
await u.po.keylessPopover.waitForMounted();
47+
48+
const redirectMap = new Map<string, number>();
49+
page.on('request', request => {
50+
const url = request.url();
51+
redirectMap.set(url, (redirectMap.get(url) || 0) + 1);
52+
expect(redirectMap.get(url)).toBeLessThanOrEqual(1);
53+
});
54+
55+
await u.page.goToRelative('/something');
56+
await u.page.waitForAppUrl('/something');
57+
});
58+
4059
test('Toggle collapse popover and claim.', async ({ page, context }) => {
4160
const u = createTestUtils({ app, page, context });
4261
await u.page.goToAppHome();
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
'use client';
22

33
import type { AccountlessApplication } from '@clerk/backend';
4+
import { useSelectedLayoutSegments } from 'next/navigation';
45
import type { PropsWithChildren } from 'react';
56
import { useEffect } from 'react';
67

78
import { canUseKeyless } from '../../utils/feature-flags';
89

910
export function KeylessCookieSync(props: PropsWithChildren<AccountlessApplication>) {
11+
const segments = useSelectedLayoutSegments();
12+
const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false;
13+
1014
useEffect(() => {
11-
if (canUseKeyless) {
15+
if (canUseKeyless && !isNotFoundRoute) {
1216
void import('../keyless-actions.js').then(m =>
1317
m.syncKeylessConfigAction({
1418
...props,
@@ -17,7 +21,7 @@ export function KeylessCookieSync(props: PropsWithChildren<AccountlessApplicatio
1721
}),
1822
);
1923
}
20-
}, []);
24+
}, [isNotFoundRoute]);
2125

2226
return props.children;
2327
}

packages/nextjs/src/app-router/client/keyless-creator-reader.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1+
import { useSelectedLayoutSegments } from 'next/navigation';
12
import React, { useEffect } from 'react';
23

34
import type { NextClerkProviderProps } from '../../types';
45
import { createOrReadKeylessAction } from '../keyless-actions';
56

67
export const KeylessCreatorOrReader = (props: NextClerkProviderProps) => {
78
const { children } = props;
9+
const segments = useSelectedLayoutSegments();
10+
const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false;
811
const [state, fetchKeys] = React.useActionState(createOrReadKeylessAction, null);
912
useEffect(() => {
13+
if (isNotFoundRoute) {
14+
return;
15+
}
1016
React.startTransition(() => {
1117
fetchKeys();
1218
});
13-
}, []);
19+
}, [isNotFoundRoute]);
1420

1521
if (!React.isValidElement(children)) {
1622
return children;

packages/nextjs/src/app-router/keyless-actions.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,28 @@ import { redirect, RedirectType } from 'next/navigation';
55

66
import { errorThrower } from '../server/errorThrower';
77
import { detectClerkMiddleware } from '../server/headers-utils';
8-
import { getKeylessCookieName } from '../server/keyless';
8+
import { getKeylessCookieName, getKeylessCookieValue } from '../server/keyless';
99
import { canUseKeyless } from '../utils/feature-flags';
1010

1111
export async function syncKeylessConfigAction(args: AccountlessApplication & { returnUrl: string }): Promise<void> {
1212
const { claimUrl, publishableKey, secretKey, returnUrl } = args;
1313
const cookieStore = await cookies();
14+
const request = new Request('https://placeholder.com', { headers: await headers() });
15+
16+
const keyless = getKeylessCookieValue(name => cookieStore.get(name)?.value);
17+
const pksMatch = keyless?.publishableKey === publishableKey;
18+
const sksMatch = keyless?.secretKey === secretKey;
19+
if (pksMatch && sksMatch) {
20+
// Return early, syncing in not needed.
21+
return;
22+
}
23+
24+
// Set the new keys in the cookie.
1425
cookieStore.set(getKeylessCookieName(), JSON.stringify({ claimUrl, publishableKey, secretKey }), {
1526
secure: true,
1627
httpOnly: true,
1728
});
1829

19-
const request = new Request('https://placeholder.com', { headers: await headers() });
20-
2130
// We cannot import `NextRequest` due to a bundling issue with server actions in Next.js 13.
2231
// @ts-expect-error Request will work as well
2332
if (detectClerkMiddleware(request)) {

0 commit comments

Comments
 (0)