۱۴۰۴/۰۷/۲۶ Nebular

مشکل stale middleware در next js

مشکل اینطوری هست که یک برنامه داریم که یک سری صفحات نیاز به لاگین ندارن ولی یک سری صفحات نیاز به لاگین دارن وقتی وارد برنامه میشیم وهنوز توکن نداریم یکبار میدلور اجرا میشه ، ولی وقتی که کاربر لاگین میکنه و توکن رو دریافت میکنم هنوز هم نمیتونیم وارد صفحاتی بشیم که به لاگین نیاز دارن ؟

این دقیقا یکی از چالش‌های رایج توی Next.js 13/14/15 وقتی با middleware + auth کار می‌کنیم.

علت‌های رایج

  1. توکن فقط توی localStorage نگه داشته شده
    • Middleware روی server اجرا میشه و دسترسی به localStorage نداره.
    • پس هر بار middleware اجرا میشه، انگار توکن وجود نداره.
      ✅ راه‌حل: توکن باید توی cookie (HttpOnly/secure) ذخیره بشه تا سرور بتونه بخونه.
  2. کوکی درست set نشده
    • ممکنه کوکی روی مسیر / یا دامنه درست ست نشده باشه.
    • یا SameSite, Secure تنظیمات مشکل داشته باشه.
  3. Middleware درست کوکی رو نمی‌خونه
    • توی Next.js باید از cookies() یا request.cookies.get("token") استفاده کنی.
    • اگر اسم کوکی با چیزی که ذخیره کردی فرق کنه → همیشه undefined برمی‌گرده.
  4. Caching یا stale middleware
    • اگه بعد از login صفحه رو hard refresh نکردی، احتمال داره کلاینت هنوز routing کش شده رو استفاده کنه.

اینجا فقط در مورد stale middleware و نکات مربوط به اون صحبت میکنیم .

ین یه مشکل واقعی توی Next.js 13/14/15 هست چون Middleware روی Edge اجرا میشه و Next/Browser بعضی وقت‌ها کوکی‌های جدید رو بلافاصله به middleware نمی‌رسونن.

چرا این اتفاق میفته؟

  1. وقتی کاربر login می‌کنه، کوکی جدید (Set-Cookie) ست میشه.
  2. مرورگر ممکنه درخواست بعدی رو از cache بگیره یا middleware هنوز نسخه قدیمی request رو ببینه.
  3. در نتیجه middleware فکر می‌کنه کوکی وجود نداره → انگار هنوز logout هستی.

1. جلوگیری از Cache در Middleware

تو باید به NextResponse بگی که نتیجه cache نشه.

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // جلوگیری از کش شدن
  res.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
  res.headers.set("Pragma", "no-cache");
  res.headers.set("Expires", "0");

  return res;
}

2. بعد از Login → Refresh کامل

بعد از اینکه سرور Set-Cookie فرستاد، مرورگر حتما باید یه redirect / hard reload به صفحه protected انجام بده تا مطمئن بشی کوکی تازه همراه request میره سمت middleware.
مثال در React:

await loginUser(credentials);
// بعد از login موفق
window.location.replace("/dashboard") // hard reload

3. استفاده از cookies().get() فقط روی Request واقعی

توی Middleware همیشه باید از:

req.cookies.get("auth_token")?.value;

استفاده کنی و نه چیزی که از سمت client بیاد.
اینطوری مطمئنی آخرین نسخه کوکی رو از request می‌گیری.


4. توی fetch ها → cache: "no-store"

اگر بعد از login داری fetch می‌زنی که دیتا رو بیاره، حتما باید cache رو غیرفعال کنی:

await fetch("/api/user", { cache: "no-store" });

نهایتا کاری که من توی پروژه انجام دادم و برای من کار کرد :

بعد از فرایند لاگین یک هارد ریلود انجام دادم

  1. بعد از لاگین:
window.location.replace(`${finalRedirect}`);
  • این باعث میشه full page reload اتفاق بیفته (برخلاف router.push که client-side navigation هست).
  • در نتیجه کوکی جدید (که بک‌اند روی login ست کرده) حتماً همراه request بعدی به سرور و middleware میره.
  • ✅ این قسمت کاملاً درسته و باعث میشه مشکل “stale middleware” تا حد زیادی حل بشه.
  1. توی middleware:
if (isProtectedPage) {
  if (!token) {
    const loginUrl = new URL(loginPage, request.url);
    loginUrl.searchParams.set("redirectTo", pathname + search);
    return NextResponse.redirect(loginUrl);
  }

  const response = NextResponse.next();
  response.headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
  response.headers.set('Pragma', 'no-cache');
  response.headers.set('Expires', '0');
  return response;
}
  • ✅ اگر توکن نباشه، ریدایرکت می‌کنی به صفحه لاگین + پارامتر redirectTo → تجربه کاربری عالی.
  • ✅ وقتی توکن هست، پاسخ رو با هدرهای ضد کش می‌دی → این باعث میشه هم مرورگر و هم edge cache (Vercel, Cloudflare, …) دیگه نسخه قدیمی رو نگه ندارن.
  • ✅ اینکه فقط روی صفحات protected هدر ضدکش می‌ذاری هم بهینه‌ست (چون همه صفحات نیاز ندارن).

این ترکیب دقیقا همون چیزیه که جلوی مشکل رو می‌گیره:

  1. window.location.replace → مطمئن میشه رفرش کامل بشه و کوکی تازه توی request بعدی همراه باشه.
  2. هدرهای ضدکش توی middleware → باعث میشه نه مرورگر و نه edge نسخه cache شده رو نگه نداره

که میشه هدر no-cache توی میدلور رو تمیز تر نوشت

  • بجای تکرار هدرها می‌تونی یه helper بسازی:
function noCacheResponse() {
  const res = NextResponse.next();
  res.headers.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
  res.headers.set("Pragma", "no-cache");
  res.headers.set("Expires", "0");
  return res;
}

و بعد استفاده کنی:

if (isProtectedPage) {
  if (!token) {
    ...
  }
  return noCacheResponse();
}

نکته ای که باید بهش دقت کرد اینکه بعد از لاگین نباید از router.replace استفاده کرد و بهتره از location.replace استفاده کنیم

این دقیقاً برمی‌گرده به تفاوت بین

  • router.replace (Client-Side Navigation)
  • window.location.replace (Full Page Reload)

رفتار router.replace

  • این متد فقط مسیر رو تغییر میده و React/Next.js همون hydration / serialization موجود رو دوباره استفاده می‌کنه.
  • چون از client-side navigation استفاده می‌کنه، کل اپ reload نمیشه.
  • وقتی اپ رو توی موبایل (کروم) مینیمایز می‌کنی و بعد دوباره میاری بالا، مرورگر در واقع صفحه cache‌شده و serialized state رو برمی‌گردونه (به جای اینکه درخواست جدید به سرور بزنه).
  • نتیجه؟ بعضی وقتا به جای محتوای تازه، داده‌های سریالایز شده یا ناقص می‌بینی (چون hydration کامل اتفاق نیفتاده).

🔹 رفتار window.location.replace

  • این یکی یه full page reload انجام میده.
  • یعنی کل جاوااسکریپت و HTML دوباره از سرور میاد.
  • در نتیجه:
    • state قدیمی / cache شده مرورگر دیگه استفاده نمیشه.
    • همیشه یک hydration جدید انجام میشه.
  • به همین دلیل وقتی دوباره اپ رو از recent apps بالا میاری، صفحه تمیز و درست لود میشه.

البته یه راه دیگه هم بود برای جلو گیری از اینکه میدلور داده های کش رو بخونه اون هم ساخت یک api route توی برنامه next js بود که بعد از لاگین به اون ریدایرکت کنی که ممکنه بعدا راجع بهش بنویسم.

window.location.href = /api/redirect?to=/dashboard;

Accept Cookies
Accept Cookies
[your-shortcode]