The backend now answers 503 with code MAINTENANCE_MODE while maintenance is on. Without this change a visitor got a red error toast over a broken page client-side, and a silently empty page server-side, because fetchReq discards the status and returns null on any failure. - lib/maintenance.js detects the state by BOTH status 503 and the error code; a bare 503 can come from a reverse proxy and is not maintenance - The axios interceptor checks it before the 401 branch, so a maintenance response never triggers the refresh-token path or logs the user out - fetchReq redirects to /maintenance, with a silentMaintenance opt-out used by getStateInfo: that one runs inside generateMetadata and while rendering the maintenance page itself, where a redirect is either ineffective or loops - redirect() works by throwing, so the try/catch blocks in the doctors, clinics and specialties pages now rethrow NEXT_REDIRECT instead of swallowing it - clinicApi.js handles 503 too; it previously rendered maintenance as a clinic with zero doctors - The page reuses the existing 404 design and is marked noindex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
نمایش حالت تعمیرات (Maintenance) در سایت عمومی
پروژه
nobat724_front
cross-repo — پرامپت همتا (که باید اول اجرا شود):
clinicpro/.claude/prompt/maintenance-mode.md
زمینه
در backend یک Maintenance Mode مرکزی پیاده میشود: وقتی ادمین آن را روشن کند، هر درخواست به /api/v1/... (بهجز چند مسیر whitelistشده) با این پاسخ برمیگردد:
HTTP/1.1 503 Service Unavailable
Retry-After: 600
Content-Type: application/json
{
"success": false,
"data": null,
"errors": [
{ "code": "MAINTENANCE_MODE", "message": "<پیام قابل ویرایش از پنل ادمین>" }
]
}
قرارداد تشخیص: status = 503 و errors[0].code === 'MAINTENANCE_MODE'. هر دو شرط باید چک شوند (503 خالی میتواند از nginx/load balancer هم بیاید).
بدون تغییر در این ریپو، رفتار فعلی این میشود:
- درخواستهای client-side: interceptor در services/api.js:94 فقط یک
toast.errorقرمز نشان میدهد و صفحه خالی/شکسته میماند - درخواستهای server-side:
fetchReqدر lib/req.js:16 روی هر خطاnullبرمیگرداند — صفحه بدون داده رندر میشود و این حالت از یک صفحه واقعاً خالی قابل تفکیک نیست
هدف: بهجای اینها یک صفحه تمیز maintenance با پیام آمده از backend.
فایلهای مرتبط
| فایل | نقش |
|---|---|
services/api.js |
axios instance + response interceptor (نقطه تشخیص client-side) |
lib/req.js |
fetchReq() server-side — الان status را دور میریزد |
app/maintenance/page.js |
جدید — صفحه تعمیرات |
components/maintenance/index.js |
جدید — UI کامل صفحه |
components/notFound/index.js |
الگوی طراحی full-page state که باید کپی شود |
app/layout.js |
<CustomToastify /> در خط 140 |
app/error.js |
error boundary فعلی |
وضعیت فعلی
services/api.js:72-98
api.interceptors.response.use(
(response) => response.data,
async (error) => {
const original = error?.config;
if (
error?.response?.status === 401 &&
original?.requireAuth &&
!original._retried &&
typeof window !== "undefined"
) {
original._retried = true;
const token = await refreshAccessToken();
if (token) {
original.headers = { ...original.headers, Authorization: `Bearer ${token}` };
return api(original);
}
handleSessionExpired();
return Promise.reject(error);
}
if (typeof window !== "undefined" && !error?.config?.skipErrorToast) {
toast.error(extractErrorMessage(error));
}
return Promise.reject(error);
}
);
lib/req.js
export const fetchReq = async (url, headers) => {
try {
const response = await axiosInstance.get(url, headers);
return response.data;
} catch (error) {
console.error("fetchReq error:", error.message);
return null;
}
};
وظایف
۱. helper مشترک تشخیص
فایل جدید lib/maintenance.js:
export const MAINTENANCE_CODE = "MAINTENANCE_MODE";
/** از یک خطای axios تشخیص میدهد که آیا پاسخ maintenance است */
export function isMaintenanceError(error) {
const res = error?.response;
if (res?.status !== 503) return false;
return res?.data?.errors?.[0]?.code === MAINTENANCE_CODE;
}
/** از body پاسخ 503 پیام را استخراج میکند */
export function maintenanceMessage(data) {
return data?.errors?.[0]?.message
?? "سامانه موقتاً در دسترس نیست. لطفاً چند دقیقه دیگر مجدداً تلاش کنید.";
}
۲. تشخیص client-side در interceptor
در services/api.js قبل از بلاک 401 (چون 503 هرگز نباید مسیر refresh token را طی کند):
if (isMaintenanceError(error)) {
if (typeof window !== "undefined") {
const msg = maintenanceMessage(error.response.data);
try { sessionStorage.setItem("maintenance_message", msg); } catch {}
if (window.location.pathname !== "/maintenance") {
window.location.replace("/maintenance");
}
}
return Promise.reject(error);
}
نکات:
- حتماً قبل از بلاک 401 — وگرنه اگر مسیر auth هم روزی 503 بدهد، وارد حلقه refresh میشود.
- هیچ toast نشان نده برای maintenance؛ ریدایرکت جایگزین آن است. مطمئن شو بلاک
toast.errorپایین اجرا نمیشود (به خاطرreturnزودهنگام). window.location.replaceعمداً استفاده شده نهrouter.push— چون interceptor خارج از React tree است و ریدایرکت باید کل state آلوده را دور بریزد. همچنینreplaceباعث میشود دکمه back کاربر را به صفحه شکسته برنگرداند.- شرط
pathname !== "/maintenance"الزامی است تا اگر خود صفحه maintenance درخواستی زد، حلقه ریدایرکت بینهایت نشود.
۳. تشخیص server-side در fetchReq
lib/req.js نباید روی 503 مثل بقیه خطاها null برگرداند. رفتار پیشنهادی: throw کردن یک خطای مشخص که در app/error.js قابل تشخیص باشد — یا (سادهتر و مطمئنتر) ریدایرکت مستقیم:
import { redirect } from "next/navigation";
import { isMaintenanceError, maintenanceMessage } from "@/lib/maintenance";
export const fetchReq = async (url, headers) => {
try {
const response = await axiosInstance.get(url, headers);
return response.data;
} catch (error) {
if (isMaintenanceError(error)) {
redirect("/maintenance");
}
console.error("fetchReq error:", error.message);
return null;
}
};
بحرانی: redirect() در Next.js با پرتاب یک خطای خاص (NEXT_REDIRECT) کار میکند. اگر fetchReq داخل یک try/catch دیگر در صفحه صدا زده شود، آن catch خطای redirect را میبلعد و ریدایرکت انجام نمیشود. همهی call siteهای fetchReq را چک کن؛ هر جا داخل try/catch است، خطای NEXT_REDIRECT باید rethrow شود (if (e?.digest?.startsWith("NEXT_REDIRECT")) throw e;).
همچنین redirect() را نمیتوان داخل generateMetadata بهدرستی استفاده کرد — آنجا فقط بگذار null برگردد و صفحه خودش ریدایرکت کند.
۴. صفحه /maintenance
app/maintenance/page.js:
- Server Component ساده که
<MaintenancePage />را رندر میکند export const dynamic = "force-dynamic"تا کش نشودgenerateMetadataصادر کند باtitleمناسب وrobots: { index: false, follow: false }— صفحه تعمیرات نباید ایندکس شود- بهتر: در همین صفحه یک بار سمت سرور
GET /api/v1/...سبک بزن (مثلاً همان endpoint سلامت یا هر endpoint عمومی) تا اگر maintenance تمام شده بود، کاربر را به/برگرداند — وگرنه کاربری که این URL را باز نگه داشته برای همیشه صفحه تعمیرات میبیند
components/maintenance/index.js:
- طراحی را از
components/notFound/index.jsکپی کن — همان ساختار، همان MUIButton، همان breakpointهای Tailwind، همان لحن فارسی. صفحه جدید با طراحی جدید نساز. - پیام: اول از
sessionStorage.getItem("maintenance_message")(client) بخوان؛ اگر نبود متن پیشفرض فارسی - دکمه «تلاش مجدد» که
window.location.href = "/"میکند - اختیاری: یک
setIntervalهر ۶۰ ثانیه که خودکار/را چک کند و اگر بالا آمد ریدایرکت کند
نکته: دکمه components/notFound/index.js:28 در حال حاضر نه onClick دارد نه href — کنترل مرده است. آن باگ را کپی نکن.
۵. جلوگیری از حلقه
- صفحه
/maintenanceنباید هیچ درخواستrequireAuthبزند - اگر لایههای دیگری هم مستقیم fetch میزنند (
services/clinicApi.jsکه native fetch است و روی!response.okاستثنا را میبلعد و آرایه خالی برمیگرداند)، آنها را هم برای 503 تطبیق بده — وگرنه صفحه کلینیک در حالت maintenance «۰ پزشک» نشان میدهد که گمراهکننده است
نکات مهم
- تشخیص فقط با ترکیب
503 + code === 'MAINTENANCE_MODE'. صرفِ 503 کافی نیست. - 503 هرگز نباید مسیر refresh token /
handleSessionExpiredرا فعال کند — کاربر نباید در حالت تعمیرات logout شود. robots: noindexروی صفحه maintenance الزامی است.CustomToastifyدر app/CustomToastify.js:6-14 برای همهی toastها آیکون تیک ثابت دارد؛ به همین دلیل هم نمایش خطای maintenance با toast مناسب نیست.- ترتیب اجرا: اول پرامپت backend (
clinicpro/.claude/prompt/maintenance-mode.md)، بعد این. برای تست، maintenance را از پنل ادمین روشن کن و سایت عمومی را باز کن. - تست دستی: (۱) صفحه اصلی (server-side render) (۲) صفحه پزشک
/doctor/[uuid](۳) یک اکشن client-side مثل جستجو (۴) خاموش کردن maintenance و اطمینان از برگشت خودکار سایت.