cloudflare-next-intl 0.8.36 → 0.8.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -246,7 +246,20 @@ export default function AuthUserProvider({ initialUser = null, children }) {
246
246
  }
247
247
  }
248
248
  const flipped = previous !== undefined && previous !== isSignedIn;
249
- const contradictsPage = previous === undefined && isSignedIn === isAuthPage;
249
+ // `contradictsPage` infers "the server rendered this page for
250
+ // the opposite auth state, resync it" from the page's own
251
+ // auth/non-auth role. On a whitelisted path that inference is
252
+ // meaningless — the page renders identically signed-in or
253
+ // signed-out and neither guard ever redirects away from it —
254
+ // so a signed-out visitor on a non-auth whitelisted page
255
+ // (`isSignedIn === isAuthPage`, both false) matched on the
256
+ // first observation and refreshed. That refresh re-primes the
257
+ // router cache, re-firing every in-viewport `<Link>` prefetch,
258
+ // which lands back here and repeats: an unbounded refresh loop
259
+ // on exactly the public landing pages whitelisting exists for.
260
+ // A real `flipped` transition still refreshes, whitelisted or
261
+ // not — that one observed an actual state change.
262
+ const contradictsPage = !isWhiteListed && previous === undefined && isSignedIn === isAuthPage;
250
263
  if (flipped || contradictsPage) {
251
264
  router.refresh();
252
265
  }
@@ -257,7 +270,7 @@ export default function AuthUserProvider({ initialUser = null, children }) {
257
270
  unsubscribe?.();
258
271
  };
259
272
  // eslint-disable-next-line react-hooks/exhaustive-deps
260
- }, [router, isAuthPage, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge]);
273
+ }, [router, isAuthPage, isWhiteListed, maxAge, sessionCookieName, refreshTokenMaxAge, refreshTokenCookieName, emailVerifiedHintCookieName, appCheckTokenCookieName, appCheckTokenMaxAge]);
261
274
  const reloadUser = useCallback(async () => {
262
275
  const { auth } = await getFirebaseAuthClient();
263
276
  const user = auth.currentUser;
@@ -225,6 +225,7 @@ export default async function updateSession(request, baseResponse, locale, rebui
225
225
  if (rawPath.startsWith('/_next') || /\.[a-zA-Z0-9]+$/.test(lastSegment)) {
226
226
  return baseResponse;
227
227
  }
228
+ const isPrefetch = isPrefetchRequest(request);
228
229
  const localePrefix = locale === config.defaultLocale ? '' : requestPrefix;
229
230
  const localeUrl = (target) => new URL(withRedirectQuery(`${localePrefix}${target === '/' ? '' : target}` || '/', request.nextUrl.search), request.url);
230
231
  // Emailed Firebase action links all arrive on the single project-wide
@@ -283,7 +284,7 @@ export default async function updateSession(request, baseResponse, locale, rebui
283
284
  }
284
285
  }
285
286
  parsed.search = request.nextUrl.search;
286
- return buildRedirect(baseResponse, parsed);
287
+ return buildRedirect(baseResponse, parsed, isPrefetch);
287
288
  }
288
289
  }
289
290
  catch {
@@ -308,7 +309,7 @@ export default async function updateSession(request, baseResponse, locale, rebui
308
309
  url.searchParams.delete(key);
309
310
  }
310
311
  }
311
- return buildRedirect(baseResponse, url);
312
+ return buildRedirect(baseResponse, url, isPrefetch);
312
313
  }
313
314
  }
314
315
  }
@@ -444,14 +445,14 @@ export default async function updateSession(request, baseResponse, locale, rebui
444
445
  response = baseResponse;
445
446
  }
446
447
  else if (!hasSession || clearInvalidSession) {
447
- response = isAuthPage ? baseResponse : buildRedirect(baseResponse, localeUrl(fa.redirectAuthPath));
448
+ response = isAuthPage ? baseResponse : buildRedirect(baseResponse, localeUrl(fa.redirectAuthPath), isPrefetch);
448
449
  }
449
450
  else if (unverifiedEmail) {
450
451
  // Checked before the auth-page redirect: an unverified signed-in
451
452
  // user must land on verifyEmailPath even if they navigated to an
452
453
  // auth page like /login — homePath is not a state they're allowed
453
454
  // to reach yet either.
454
- response = buildRedirect(baseResponse, localeUrl(fa.verifyEmailPath));
455
+ response = buildRedirect(baseResponse, localeUrl(fa.verifyEmailPath), isPrefetch);
455
456
  }
456
457
  else if (isAuthPage || (isVerifyEmailPage && decodeJwtPayload(token)?.email_verified === true)) {
457
458
  // A verified user has no reason to be on verifyEmailPath either —
@@ -468,7 +469,7 @@ export default async function updateSession(request, baseResponse, locale, rebui
468
469
  // boolean `user.emailVerified`. The two disagreeing caused an
469
470
  // infinite client<->server redirect loop on this exact page when a
470
471
  // token's claim was merely absent rather than `false`.
471
- response = buildRedirect(baseResponse, localeUrl(fa.homePath));
472
+ response = buildRedirect(baseResponse, localeUrl(fa.homePath), isPrefetch);
472
473
  }
473
474
  else {
474
475
  response = baseResponse;
@@ -499,7 +500,27 @@ export default async function updateSession(request, baseResponse, locale, rebui
499
500
  return response;
500
501
  }
501
502
  /** A redirect response can't carry forward `baseResponse`'s rewrite/next decision, so this copies its cookies/headers across instead of dropping them. */
502
- function buildRedirect(baseResponse, url) {
503
+ // A router prefetch must never be answered with a redirect. Next's segment
504
+ // cache stores the entry under the REQUESTED url while `fetch` transparently
505
+ // follows the 3xx, so the entry it caches describes a different route than the
506
+ // key it is filed under; the router then keeps re-requesting it, which
507
+ // re-redirects, an unbounded prefetch loop that hammers the origin (every
508
+ // signed-out page carrying a `<Link>` to a guarded route reproduced it).
509
+ // An empty 204 is treated as an un-cacheable prefetch miss instead: the router
510
+ // backs off, and the guard still runs in full on the real navigation, which
511
+ // is never a prefetch.
512
+ function isPrefetchRequest(request) {
513
+ return request.headers.get('next-router-prefetch') === '1'
514
+ || request.headers.get('purpose') === 'prefetch'
515
+ || request.headers.get('x-purpose') === 'prefetch';
516
+ }
517
+ function buildRedirect(baseResponse, url, isPrefetch = false) {
518
+ if (isPrefetch) {
519
+ return new NextResponse(null, {
520
+ status: 204,
521
+ headers: { 'Cache-Control': 'private, no-cache, no-store, max-age=0, must-revalidate' },
522
+ });
523
+ }
503
524
  const redirectResponse = NextResponse.redirect(url);
504
525
  baseResponse.cookies.getAll().forEach((cookie) => redirectResponse.cookies.set(cookie));
505
526
  baseResponse.headers.forEach((value, key) => redirectResponse.headers.set(key, value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.8.36",
3
+ "version": "0.8.38",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",