Skip to content

Commit

Permalink
fix(nextjs): Fix infinite redirect loop when page does not exist (#5073)
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef authored Feb 3, 2025
1 parent f51ecdc commit 573ed86
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/twenty-doors-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/nextjs': patch
---

Bug fix: On keyless avoid infinite redirect loop when page does not exist and application is attempting to sync state with middleware.
19 changes: 19 additions & 0 deletions integration/tests/next-quickstart-keyless.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ test.describe('Keyless mode @quickstart', () => {
await app.teardown();
});

test('Navigates to non existed page (/_not-found) without a infinite redirect loop.', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.po.keylessPopover.waitForMounted();

const redirectMap = new Map<string, number>();
page.on('request', request => {
const url = request.url();
redirectMap.set(url, (redirectMap.get(url) || 0) + 1);
expect(redirectMap.get(url)).toBeLessThanOrEqual(1);
});

await u.page.goToRelative('/something');
await u.page.waitForAppUrl('/something');
});

test('Toggle collapse popover and claim.', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToAppHome();
Expand Down
8 changes: 6 additions & 2 deletions packages/nextjs/src/app-router/client/keyless-cookie-sync.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
'use client';

import type { AccountlessApplication } from '@clerk/backend';
import { useSelectedLayoutSegments } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { useEffect } from 'react';

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

export function KeylessCookieSync(props: PropsWithChildren<AccountlessApplication>) {
const segments = useSelectedLayoutSegments();
const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false;

useEffect(() => {
if (canUseKeyless) {
if (canUseKeyless && !isNotFoundRoute) {
void import('../keyless-actions.js').then(m =>
m.syncKeylessConfigAction({
...props,
Expand All @@ -17,7 +21,7 @@ export function KeylessCookieSync(props: PropsWithChildren<AccountlessApplicatio
}),
);
}
}, []);
}, [isNotFoundRoute]);

return props.children;
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useEffect } from 'react';

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

export const KeylessCreatorOrReader = (props: NextClerkProviderProps) => {
const { children } = props;
const segments = useSelectedLayoutSegments();
const isNotFoundRoute = segments[0]?.startsWith('/_not-found') || false;
const [state, fetchKeys] = React.useActionState(createOrReadKeylessAction, null);
useEffect(() => {
if (isNotFoundRoute) {
return;
}
React.startTransition(() => {
fetchKeys();
});
}, []);
}, [isNotFoundRoute]);

if (!React.isValidElement(children)) {
return children;
Expand Down
15 changes: 12 additions & 3 deletions packages/nextjs/src/app-router/keyless-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,28 @@ import { redirect, RedirectType } from 'next/navigation';

import { errorThrower } from '../server/errorThrower';
import { detectClerkMiddleware } from '../server/headers-utils';
import { getKeylessCookieName } from '../server/keyless';
import { getKeylessCookieName, getKeylessCookieValue } from '../server/keyless';
import { canUseKeyless } from '../utils/feature-flags';

export async function syncKeylessConfigAction(args: AccountlessApplication & { returnUrl: string }): Promise<void> {
const { claimUrl, publishableKey, secretKey, returnUrl } = args;
const cookieStore = await cookies();
const request = new Request('https://placeholder.com', { headers: await headers() });

const keyless = getKeylessCookieValue(name => cookieStore.get(name)?.value);
const pksMatch = keyless?.publishableKey === publishableKey;
const sksMatch = keyless?.secretKey === secretKey;
if (pksMatch && sksMatch) {
// Return early, syncing in not needed.
return;
}

// Set the new keys in the cookie.
cookieStore.set(getKeylessCookieName(), JSON.stringify({ claimUrl, publishableKey, secretKey }), {
secure: true,
httpOnly: true,
});

const request = new Request('https://placeholder.com', { headers: await headers() });

// We cannot import `NextRequest` due to a bundling issue with server actions in Next.js 13.
// @ts-expect-error Request will work as well
if (detectClerkMiddleware(request)) {
Expand Down

0 comments on commit 573ed86

Please sign in to comment.