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/dynamic-imports-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

Use dynamic imports for next/headers, next/navigation, and next-intl/server in the client module to avoid AsyncLocalStorage poisoning during next.config.ts resolution
25 changes: 18 additions & 7 deletions core/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { BigCommerceAuthError, createClient } from '@bigcommerce/catalyst-client';
import { headers } from 'next/headers';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

are these (next/headers, next/navigation, next-intl/server) the only affected paths? or will we need to worry about others later?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question! Unfortunately, these Next.js API's rely on an AsyncLocalStorage store that is not resolving correctly when these API's are called in next.config.*. Any Next.js API statically imported into this file that rely on AsyncLocalStorage would re-introduce the bug... that said, Next.js opened vercel/next.js#90711 to fix this, and once released, I should be able to revert this PR 👍

// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { redirect } from 'next/navigation';
import { getLocale as getServerLocale } from 'next-intl/server';

import { getChannelIdFromLocale } from '../channels.config';
import { backendUserAgent } from '../user-agent';

// next/headers, next/navigation, and next-intl/server are imported dynamically
// (via `import()`) rather than statically. Static imports cause these modules to
// be evaluated during module graph resolution when next.config.ts imports this
// file, which poisons the process-wide AsyncLocalStorage context (pnpm symlinks
// create two separate singleton instances of next/headers). Dynamic imports
// defer module loading to call time, after Next.js has fully initialized.
//
// During config resolution, the dynamic import of next-intl/server succeeds but
// getLocale() throws ("not supported in Client Components") — the try/catch
// below absorbs this gracefully, and getChannelId falls back to defaultChannelId.

const getLocale = async () => {
try {
const locale = await getServerLocale();
const { getLocale: getServerLocale } = await import('next-intl/server');

return locale;
return await getServerLocale();
} catch {
/**
* Next-intl `getLocale` only works on the server, and when middleware has run.
*
* Instances when `getLocale` will not work:
* - Requests during next.config.ts resolution
* - Requests in middlewares
* - Requests in `generateStaticParams`
* - Request in api routes
Expand Down Expand Up @@ -45,6 +53,7 @@ export const client = createClient({
const locale = await getLocale();

if (fetchOptions?.cache && ['no-store', 'no-cache'].includes(fetchOptions.cache)) {
const { headers } = await import('next/headers');
const ipAddress = (await headers()).get('X-Forwarded-For');

if (ipAddress) {
Expand All @@ -61,8 +70,10 @@ export const client = createClient({
headers: requestHeaders,
};
},
onError: (error, queryType) => {
onError: async (error, queryType) => {
if (error instanceof BigCommerceAuthError && queryType === 'query') {
const { redirect } = await import('next/navigation');

redirect('/api/auth/signout');
}
},
Expand Down
Loading