@@ -4,6 +4,7 @@ import { z } from 'zod';
44import { client } from '~/client' ;
55import { graphql } from '~/client/graphql' ;
66import { revalidate } from '~/client/revalidate-target' ;
7+ import { defaultLocale } from '~/i18n/locales' ;
78import { getVisitIdCookie , getVisitorIdCookie } from '~/lib/analytics/bigcommerce' ;
89import { sendProductViewedEvent } from '~/lib/analytics/bigcommerce/data-events' ;
910import { kvKey , STORE_STATUS_KEY } from '~/lib/kv/keys' ;
@@ -248,6 +249,15 @@ const getRouteInfo = async (request: NextRequest, event: NextFetchEvent) => {
248249 // For route resolution parity, we need to also include query params, otherwise certain redirects will not work.
249250 const pathname = clearLocaleFromPath ( request . nextUrl . pathname + request . nextUrl . search , locale ) ;
250251
252+ // Skip BigCommerce route resolution for /404 paths to avoid GraphQL 400 errors
253+ // This is a defensive check in case the early return in withRoutes doesn't catch it
254+ if ( pathname === '/404' || pathname === '/404/' || pathname . startsWith ( '/404?' ) ) {
255+ return {
256+ route : undefined ,
257+ status : undefined ,
258+ } ;
259+ }
260+
251261 let [ routeCache , statusCache ] = await kv . mget < RouteCache | StorefrontStatusCache > (
252262 kvKey ( pathname , channelId ) ,
253263 kvKey ( STORE_STATUS_KEY , channelId ) ,
@@ -290,6 +300,50 @@ export const withRoutes: MiddlewareFactory = () => {
290300 return async ( request , event ) => {
291301 const locale = request . headers . get ( 'x-bc-locale' ) ?? '' ;
292302
303+ // Check the original pathname BEFORE any locale processing
304+ // This catches /404 paths regardless of locale prefix
305+ const originalPathname = request . nextUrl . pathname ;
306+ const pathnameWithQuery = request . nextUrl . pathname + request . nextUrl . search ;
307+
308+ // Skip BigCommerce route resolution for /404 path to avoid GraphQL 400 errors on Vercel
309+ // This path should be handled by Next.js's not-found page via the catch-all route
310+ // Check the original pathname to catch /404 before locale processing
311+ // Also check the pathname after locale processing in case withIntl rewrote it
312+ const is404Path =
313+ originalPathname === '/404' ||
314+ originalPathname === '/404/' ||
315+ originalPathname . endsWith ( '/404' ) ||
316+ originalPathname . endsWith ( '/404/' ) ||
317+ pathnameWithQuery . startsWith ( '/404' ) ||
318+ pathnameWithQuery . startsWith ( '/404/' ) ;
319+
320+ if ( is404Path ) {
321+ // Use the locale from header, or default locale if not set
322+ const targetLocale = locale || defaultLocale ;
323+ const url404 = new URL ( `/${ targetLocale } /404` , request . url ) ;
324+
325+ url404 . search = request . nextUrl . search ;
326+
327+ return NextResponse . rewrite ( url404 ) ;
328+ }
329+
330+ // Also check after locale processing in case withIntl already rewrote the URL
331+ const { pathname : processedPathname } = new URL ( request . url ) ;
332+ const processedCleanPathName = clearLocaleFromPath ( processedPathname , locale ) ;
333+
334+ if ( processedCleanPathName === '/404' || processedCleanPathName === '/404/' ) {
335+ const targetLocale = locale || defaultLocale ;
336+ const url404 = new URL ( `/${ targetLocale } /404` , request . url ) ;
337+
338+ url404 . search = request . nextUrl . search ;
339+
340+ return NextResponse . rewrite ( url404 ) ;
341+ }
342+
343+ // Get pathname for route processing (reuse processedPathname as pathname)
344+ const pathname = processedPathname ;
345+ const cleanPathName = processedCleanPathName ;
346+
293347 const { route, status } = await getRouteInfo ( request , event ) ;
294348
295349 if ( status === 'MAINTENANCE' ) {
@@ -403,10 +457,6 @@ export const withRoutes: MiddlewareFactory = () => {
403457 }
404458
405459 default : {
406- const { pathname } = new URL ( request . url ) ;
407-
408- const cleanPathName = clearLocaleFromPath ( pathname , locale ) ;
409-
410460 url = `/${ locale } ${ cleanPathName } ` ;
411461 }
412462 }
0 commit comments