Skip to content

feat(core): add X-Correlation-ID header, cache guest queries separately#2910

Open
jorgemoya wants to merge 4 commits intocanaryfrom
catalyst-1787-unstable-cache-requests
Open

feat(core): add X-Correlation-ID header, cache guest queries separately#2910
jorgemoya wants to merge 4 commits intocanaryfrom
catalyst-1787-unstable-cache-requests

Conversation

@jorgemoya
Copy link
Contributor

@jorgemoya jorgemoya commented Mar 6, 2026

What/Why?

All GraphQL requests now include an X-Correlation-ID header containing a UUID that is stable for the duration of a single page render (via React.cache), making it easier to trace and correlate all requests made during a single render in server logs.

Guest (unauthenticated) queries are now cached using unstable_cache with the configured revalidation interval, while authenticated requests continue to use cache: 'no-store'. This separates cacheable public data from session-specific data, improving performance for unauthenticated visitors. The X-Forwarded-For and True-Client-IP headers are only forwarded on uncached (no-store) requests since they are unavailable inside unstable_cache.

Testing

Locally + E2E

Screenshot 2026-03-09 at 12 31 31 PM

Migration

Step 1: Add the correlation ID helper

Create core/client/correlation-id.ts:

import { cache } from 'react';

/**
 * Returns a stable correlation ID for the current request.
 * React.cache ensures the same UUID is returned for all fetches within a
 * single page render, while being unique across renders/requests.
 */
export const getCorrelationId = cache((): string => crypto.randomUUID());

Step 2: Update core/client/index.ts

Update the beforeRequest hook to add the X-Correlation-ID header to all requests and to only forward X-Forwarded-For / True-Client-IP on uncached requests:

+ import { getCorrelationId } from './correlation-id';

  export const client = createClient({
    ...
    beforeRequest: async (fetchOptions) => {
      const requestHeaders: Record<string, string> = {};

-     try {
-       const ipAddress = (await headers()).get('X-Forwarded-For');
-       if (ipAddress) {
-         requestHeaders['X-Forwarded-For'] = ipAddress;
-         requestHeaders['True-Client-IP'] = ipAddress;
-       }
-     } catch {
-       // Not in a request context
-     }
+     if (fetchOptions?.cache && ['no-store', 'no-cache'].includes(fetchOptions.cache)) {
+       try {
+         // headers() is a dynamic API unavailable inside unstable_cache; skip IP forwarding in that context
+         const ipAddress = (await headers()).get('X-Forwarded-For');
+         if (ipAddress) {
+           requestHeaders['X-Forwarded-For'] = ipAddress;
+           requestHeaders['True-Client-IP'] = ipAddress;
+         }
+       } catch {
+         // Not in a request context (e.g. inside unstable_cache); IP forwarding not available
+       }
+     }
+
+     requestHeaders['X-Correlation-ID'] = getCorrelationId();

      return { headers: requestHeaders };
    },
  });

Step 3: Wrap guest queries with unstable_cache

For each page data file, wrap the guest (unauthenticated) fetch in unstable_cache and branch on whether a customerAccessToken is present. Example pattern:

+ import { unstable_cache } from 'next/cache';
  import { cache } from 'react';
+ import { revalidate } from '~/client/revalidate-target';

+ const getCachedPageData = unstable_cache(
+   async (locale: string, ...args) => {
+     const { data } = await client.fetch({
+       document: PageQuery,
+       variables: { ... },
+       locale,
+       fetchOptions: { cache: 'no-store' },
+     });
+     return data;
+   },
+   ['cache-key'],
+   { revalidate },
+ );

  export const getPageData = cache(
-   async (locale: string, customerAccessToken?: string) => {
-     const { data } = await client.fetch({
-       document: PageQuery,
-       locale,
-       fetchOptions: { cache: 'no-store' },
-     });
-     return data;
-   },
+   async (locale: string, customerAccessToken?: string) => {
+     if (customerAccessToken) {
+       const { data } = await client.fetch({
+         document: PageQuery,
+         customerAccessToken,
+         locale,
+         fetchOptions: { cache: 'no-store' },
+       });
+       return data;
+     }
+     return getCachedPageData(locale);
+   },
  );

Future path with Next 16

We will eventually migrate to use cacheComponents, so this migration to unstable_cache will provide as the foundation of caching request functions in Catalyst going forward. In a future scenario, it will be as simple as replacing unstable_cache for the use cache directive.

-const getCachedPageData = unstable_cache(
-  async (locale: string, currencyCode?: CurrencyCode) => {
-    const { data } = await client.fetch({
-      document: HomePageQuery,
-      variables: { currencyCode },
-      locale,
-      fetchOptions: { cache: 'no-store' },
-    });
-
-    return data;
-  },
-  ['get-page-data'],
-  { revalidate },
-);
-
-export const getPageData = cache(
-  async (locale: string, currencyCode?: CurrencyCode, customerAccessToken?: string) => {
-    if (customerAccessToken) {
-      const { data } = await client.fetch({
-        document: HomePageQuery,
-        customerAccessToken,
-        variables: { currencyCode },
-        locale,
-        fetchOptions: { cache: 'no-store' },
-      });
-
-      return data;
-    }
-
-    return getCachedPageData(locale, currencyCode);
-  },
-);
+async function getCachedPageData(locale: string, currencyCode?: CurrencyCode) {
+  'use cache';
+  cacheLife({revalidate});
+  cacheTag('page-data');
+
+  const { data } = await client.fetch({
+    document: HomePageQuery,
+    variables: { currencyCode },
+    locale,
+    fetchOptions: { cache: 'no-store' },
+  });
+
+  return data;
+}
+
+export async function getPageData(
+  locale: string,
+  currencyCode?: CurrencyCode,
+  customerAccessToken?: string,
+) {
+  if (customerAccessToken) {
+    const { data } = await client.fetch({
+      document: HomePageQuery,
+      customerAccessToken,
+      variables: { currencyCode },
+      locale,
+      fetchOptions: { cache: 'no-store' },
+    });
+
+    return data;
+  }
+
+  return getCachedPageData(locale, currencyCode);
+}

@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

🦋 Changeset detected

Latest commit: 173377f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@bigcommerce/catalyst-core Minor
@bigcommerce/catalyst-client Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
catalyst Ready Ready Preview, Comment Mar 20, 2026 3:43pm

Request Review

@jorgemoya jorgemoya force-pushed the catalyst-1787-unstable-cache-requests branch from 4daa258 to b4cfed3 Compare March 6, 2026 18:31
@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

Bundle Size Report

Comparing against baseline from 59873b1 (2026-03-20).

No bundle size changes detected.

@github-actions
Copy link
Contributor

Unlighthouse Performance Comparison — Vercel

Comparing PR preview deployment Unlighthouse scores vs production Unlighthouse scores.

Summary Score

Aggregate score across all categories as reported by Unlighthouse.

Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Score 92 94 91 95

Category Scores

Category Prod Desktop Prod Mobile Preview Desktop Preview Mobile
Performance 77 90 79 85
Accessibility 95 95 95 95
Best Practices 100 100 95 100
SEO 100 100 100 100

Core Web Vitals

Metric Prod Desktop Prod Mobile Preview Desktop Preview Mobile
LCP 3.5 s 3.5 s 3.2 s 4.3 s
CLS 0 0 0.002 0.001
FCP 1.2 s 1.2 s 1.1 s 1.2 s
TBT 0 ms 30 ms 10 ms 0 ms
Max Potential FID 50 ms 110 ms 60 ms 50 ms
Time to Interactive 3.6 s 3.6 s 4.0 s 4.3 s

Full Unlighthouse report →

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant