Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/plain-candies-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Fix server-side cache revalidation for Next.js when transitioning from `active` to `pending` session
6 changes: 0 additions & 6 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2502,14 +2502,8 @@ describe('Clerk singleton', () => {

// Verify hooks were called
await waitFor(() => {
expect(mockOnBeforeSetActive).toHaveBeenCalledTimes(1);
expect(mockOnAfterSetActive).toHaveBeenCalledTimes(1);
});

// Verify that onAfterSetActive was called after onBeforeSetActive
const beforeCallTime = mockOnBeforeSetActive.mock.invocationCallOrder[0];
const afterCallTime = mockOnAfterSetActive.mock.invocationCallOrder[0];
expect(afterCallTime).toBeGreaterThan(beforeCallTime);
});
});
});
7 changes: 1 addition & 6 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2293,19 +2293,14 @@ export class Clerk implements ClerkInterface {

const hasTransitionedToPendingStatus = this.session.status === 'active' && session?.status === 'pending';
if (hasTransitionedToPendingStatus) {
const onBeforeSetActive: SetActiveHook =
typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function'
? window.__unstable__onBeforeSetActive
: noop;

const onAfterSetActive: SetActiveHook =
typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function'
? window.__unstable__onAfterSetActive
: noop;

// Execute hooks to update server authentication context and trigger
// page protections in clerkMiddleware or server components
void onBeforeSetActive()?.then?.(() => void onAfterSetActive());
void onAfterSetActive();
}
Comment on lines 2294 to 2304
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Please add a regression test to cover the active→pending transition path

This behavior is critical for Next.js SSR protections. Add tests to assert:

  • updateClient triggers the onAfterSetActive hook exactly once when session transitions active→pending.
  • No hook call when status remains active or becomes active→active/pending→pending.
  • Hook is invoked non-blocking (no await).

You can search for other hook usages that might still be awaited (potentially causing similar issues) with this script:


🏁 Script executed:

#!/bin/bash
# Find direct uses of these hooks and how they are called
rg -n -C3 "__unstable__on(Before|After)SetActive|onBeforeSetActive|onAfterSetActive"
# Specifically look for awaited invocations of onAfterSetActive
rg -n -C2 -P '\bawait\s+onAfterSetActive\s*\('
# And fire-and-forget patterns
rg -n -C2 -P 'void\s+onAfterSetActive\s*\(|Promise\.resolve\s*\(\s*onAfterSetActive\s*\('

Length of output: 25498


Add regression tests for updateClient active→pending transition and review other hook usages

Please add a regression test in packages/clerk-js/src/core/__tests__/clerk.test.ts to cover the active→pending transition in updateClient:

  • Verify that onAfterSetActive is called exactly once when session.status changes from "active" to "pending".
  • Verify that no hook is called when the status remains "active" or transitions "pending""pending".
  • Verify that updateClient does not await the hook (i.e. it resolves before the hook’s returned promise).

Additionally, I spotted two awaited invocations of onAfterSetActive in packages/clerk-js/src/core/clerk.ts (around lines 532 and 1395). Please review those code paths to confirm whether they’re intentionally blocking or should also use a fire-and-forget pattern, and add tests or refactor as needed.

🤖 Prompt for AI Agents
In packages/clerk-js/src/core/clerk.ts around lines 2294–2304, add regression
tests in packages/clerk-js/src/core/__tests__/clerk.test.ts to cover the
updateClient active→pending transition: create a test that sets
window.__unstable__onAfterSetActive to a jest mock that returns a Promise and
assert it is called exactly once when session.status changes from "active" to
"pending", assert no hook is called when status stays "active" or transitions
"pending"→"pending", and assert updateClient does not await the hook by
resolving before the hook's promise settles (use a controllable promise and
check updateClient resolved prior to its resolution). Also review and adjust the
other two usages of onAfterSetActive in packages/clerk-js/src/core/clerk.ts
(around lines ~532 and ~1395): inspect whether they intentionally await the
hook; if they should be non-blocking, change those awaits to fire-and-forget
(call the hook without awaiting and handle errors via .catch logging) and add
tests demonstrating non-blocking behavior for those code paths.


// Note: this might set this.session to null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Header } from '@/ui/elements/Header';
import { OrganizationPreview } from '@/ui/elements/OrganizationPreview';
import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView';
import { Add } from '@/ui/icons';
import { useRouter } from '@/ui/router';
import { handleError } from '@/ui/utils/errorHandler';

type ChooseOrganizationScreenProps = {
Expand Down Expand Up @@ -106,6 +107,7 @@ export const ChooseOrganizationScreen = (props: ChooseOrganizationScreenProps) =
const MembershipPreview = (props: { organization: OrganizationResource }) => {
const { user } = useUser();
const card = useCardState();
const { navigate } = useRouter();
const { redirectUrlComplete } = useTaskChooseOrganizationContext();
const { isLoaded, setActive } = useOrganizationList();
const { t } = useLocalizations();
Expand All @@ -119,7 +121,10 @@ const MembershipPreview = (props: { organization: OrganizationResource }) => {
try {
await setActive({
organization,
redirectUrl: redirectUrlComplete,
navigate: async () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small fix - atm the warnMissingPendingTaskHandlers utility is logging a warning when TaskChooseOrganization executes setActive with a pending session, but without using navigate

// TODO(after-auth) ORGS-779 - Handle next tasks
await navigate(redirectUrlComplete);
},
});
} catch (err) {
if (!isClerkAPIResponseError(err)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Form } from '@/ui/elements/Form';
import { FormButtonContainer } from '@/ui/elements/FormButtons';
import { FormContainer } from '@/ui/elements/FormContainer';
import { Header } from '@/ui/elements/Header';
import { useRouter } from '@/ui/router';
import { createSlug } from '@/ui/utils/createSlug';
import { handleError } from '@/ui/utils/errorHandler';
import { useFormControl } from '@/ui/utils/useFormControl';
Expand All @@ -19,6 +20,7 @@ type CreateOrganizationScreenProps = {

export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => {
const card = useCardState();
const { navigate } = useRouter();
const { redirectUrlComplete } = useTaskChooseOrganizationContext();
const { createOrganization, isLoaded, setActive } = useOrganizationList({
userMemberships: organizationListParams.userMemberships,
Expand Down Expand Up @@ -47,7 +49,10 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =

await setActive({
organization,
redirectUrl: redirectUrlComplete,
navigate: async () => {
// TODO(after-auth) ORGS-779 - Handle next tasks
await navigate(redirectUrlComplete);
},
});
} catch (err) {
handleError(err, [nameField, slugField], card.setError);
Expand Down