111 lines
6.7 KiB
Markdown
111 lines
6.7 KiB
Markdown
# رفع خطای `TypeError: fetch failed` در sitemap هنگام دیپلوی (Docker build)
|
||
|
||
## پروژه
|
||
|
||
`nobat724_front`
|
||
|
||
## زمینه
|
||
|
||
هنگام دیپلوی روی سرور (Coolify، Docker multi-stage build) در لاگ build این خطا ظاهر میشود:
|
||
|
||
```
|
||
#17 192.1 Error fetching sitemap data from /api/v1/clinics: TypeError: fetch failed
|
||
```
|
||
|
||
`#17` همان مرحلهی builder در `Dockerfile` است (`RUN npm run build`، خط ۵۵). یعنی این خطا در **زمان build** رخ میدهد، نه زمان اجرا.
|
||
|
||
## مشکل / هدف
|
||
|
||
`app/sitemap.js` یک metadata route است. Next.js هنگام `next build` تلاش میکند این route را **prerender** کند و در همان لحظه تابع `sitemap()` اجرا میشود؛ این تابع از طریق `fetchAllPages()` به `NEXT_PUBLIC_API_URL` (یعنی `api.clinic-pro.ir`) درخواست `fetch` میزند.
|
||
|
||
کانتینر build در Coolify به این API دسترسی شبکهای ندارد (شبکه build ایزوله است / DNS در دسترس نیست) → `fetch` با `TypeError: fetch failed` شکست میخورد.
|
||
|
||
خطا داخل `try/catch` تابع `fetchAllPages` گرفته میشود (خط ۷۸–۸۰)، پس build **کرش نمیکند** ولی نتیجهاش این است که sitemap تولیدشده **خالی** است (فقط صفحات استاتیک، بدون هیچ URL پزشک/کلینیک/بلاگ). این هم لاگ خطای آزاردهنده میدهد و هم SEO را خراب میکند.
|
||
|
||
**هدف:** sitemap در زمان build اصلاً به API وصل نشود؛ دادهها در زمان **اجرا (request-time)** روی سرور production گرفته شوند — جایی که کانتینر runtime به API دسترسی دارد.
|
||
|
||
## فایلهای مرتبط
|
||
|
||
| فایل | نقش |
|
||
|------|-----|
|
||
| `app/sitemap.js` | تولید sitemap؛ محل fetch در زمان build |
|
||
| `Dockerfile` | مرحلهی builder خط ۵۵ (`RUN npm run build`) جایی که خطا رخ میدهد — فقط برای درک، تغییر نمیکند |
|
||
| `app/robots.js` | مشابه sitemap؛ ولی fetch ندارد — احتمالاً نیاز به تغییر نیست، فقط چک شود |
|
||
|
||
## وضعیت فعلی
|
||
|
||
`app/sitemap.js` هیچ `export const dynamic` یا `revalidate` ندارد، پس Next آن را کاندید static prerender در build میداند. تابع fetch:
|
||
|
||
```js
|
||
// app/sitemap.js — خط 56
|
||
async function fetchAllPages(path, extraParams = {}) {
|
||
if (!API_URL) return [];
|
||
const results = [];
|
||
try {
|
||
for (let page = 1; page <= MAX_PAGES; page++) {
|
||
const search = new URLSearchParams({
|
||
...extraParams,
|
||
page: String(page),
|
||
limit: String(PAGE_LIMIT),
|
||
});
|
||
const res = await fetch(`${API_URL}${path}?${search.toString()}`, {
|
||
next: { revalidate: 3600 },
|
||
});
|
||
if (!res.ok) break;
|
||
// ...
|
||
}
|
||
} catch (error) {
|
||
console.error(`Error fetching sitemap data from ${path}:`, error);
|
||
}
|
||
return results;
|
||
}
|
||
|
||
// خط 140
|
||
export default async function sitemap() { /* ... */ }
|
||
```
|
||
|
||
## وظایف
|
||
|
||
### ۱. اجبار sitemap به رندر در زمان اجرا (نه build)
|
||
|
||
بالای `app/sitemap.js` (بعد از importها) این دو خط را اضافه کن تا Next هرگز sitemap را در build فچ نکند و همیشه per-request روی سرور production تولید شود:
|
||
|
||
```js
|
||
export const dynamic = 'force-dynamic';
|
||
export const revalidate = 3600; // کش ۱ ساعته در لایهی سرور
|
||
```
|
||
|
||
> نکته: تابع فعلاً از `await headers()` استفاده میکند که باید route را dynamic کند، اما لاگ build ثابت میکند که همچنان در build اجرا میشود. `force-dynamic` این را قطعی میکند. بعد از افزودن آن، بلوک `next: { revalidate: 3600 }` داخل `fetch` را نگهدار — با `force-dynamic` هم بیضرر است.
|
||
|
||
### ۲. مقاومسازی fetch با timeout
|
||
|
||
اگر در زمان اجرا API کند یا موقتاً down باشد، هر صفحهی sitemap نباید بینهایت منتظر بماند. به `fetch` یک timeout اضافه کن:
|
||
|
||
```js
|
||
const res = await fetch(`${API_URL}${path}?${search.toString()}`, {
|
||
next: { revalidate: 3600 },
|
||
signal: AbortSignal.timeout(8000), // 8s برای هر صفحه
|
||
});
|
||
```
|
||
|
||
`try/catch` موجود (خط ۷۸) خطای timeout را هم میگیرد، پس رفتار fail-safe فعلی (بازگشت آرایهی جمعشده تا آن لحظه) حفظ میشود.
|
||
|
||
### ۳. (اختیاری) لاگ تمیزتر برای شبکهی در دسترسنبودن
|
||
|
||
پیام خطای فعلی خام است. برای اینکه در آینده گیجکننده نباشد، پیام را کمی روشنتر کن (فقط پیام، منطق دستنخورده):
|
||
|
||
```js
|
||
} catch (error) {
|
||
console.error(`[sitemap] failed to fetch ${path} (page skipped):`, error?.message || error);
|
||
}
|
||
```
|
||
|
||
## نکات مهم
|
||
|
||
- **ریشهی واقعی شبکه build**: `NEXT_PUBLIC_API_URL` در `Dockerfile` بهعنوان build arg پاس داده میشود چون برای inline شدن در bundle کلاینت لازم است — این درست است و نباید حذف شود. مشکل صرفاً این است که *در حین build نباید به آن fetch زده شود*. راهحل بالا همین را حل میکند؛ **نیازی به تغییر Dockerfile نیست**.
|
||
- بعد از این تغییر، sitemap فقط زمانی که خزنده یا کاربر `/sitemap.xml` را روی سرور production باز کند تولید میشود؛ آنجا کانتینر runtime به `api.clinic-pro.ir` دسترسی دارد.
|
||
- معماری multi-domain حفظ شود: `sitemap()` از `await headers()` برای تشخیص host/شهر استفاده میکند — با `force-dynamic` این هدرها در زمان اجرا در دسترساند (در build نبودند). این یک دلیل اضافه برای درست بودن `force-dynamic` است.
|
||
- `output: 'standalone'` در `next.config.js` فعال است؛ route داینامیک در سرور standalone بدون مشکل کار میکند.
|
||
- تست محلی: `npm run build` باید بدون خطای `fetch failed` تمام شود؛ سپس `npm run start` و باز کردن `http://yazd-nobat.localhost:3000/sitemap.xml` باید URLهای پزشک/کلینیک را نشان دهد (با API در دسترس).
|
||
- `app/robots.js` را چک کن که fetch نداشته باشد؛ اگر ندارد دستنخورده بماند.
|