Server Actions بهجای API در Next.js: مرز امنیتی، سقف ۱ مگابایت و خطای Failed to find Server Action
نویسنده: آرش لطیفی
راهنمای واقعی استفاده از Server Actions در Next.js 16: چه زمانی بهجای Route Handler، چه پروتکتهای امنیتی فریمورک (CSRF، bodySizeLimit، allowedOrigins) و رفع خطای Failed to find Server Action بعد از هر دیپلوی.
TL;DR — Server Action یک endpoint عمومی POST است که implementationاش را هیچکس نمیبیند ولی خودش قابل فراخوانی است؛ پس هر اکشن را ورودی نامعتبر فرض کن. فریمورک سه محافظت آماده دارد (چک Origin، سقف ۱ مگابایتی body، کلید رمزنگاری اکشنها) و بعد از هر دیپلوی باید مراقب خطای «Failed to find Server Action» برای کاربرانی که هنوز بیلد قدیمی را باز دارند باشی.
سوال «Server Action یا Route Handler؟» هنوز یکی از پرتکرارترین سوالهای Stack Overflow درباره Next.js است — نمونهاش تاپیک «Server actions vs API routes - when to use what». جواب کوتاهش این است: اکشن برای mutation از فرمهای خودت، Route Handler برای endpoint عمومی. ولی پرانتزهای این جواب، همان جایی است که پروژهها باگ امنیتی و باگ پروداکشن میگیرند. طبق داکیومنت رسمی 16.4 اینها را مرور کردم.
هر Server Action یک ورودی نامعتبر است
مهمترین جمله داکیومنت امنیت Server Actions این است: Treat every action as an untrusted entry point. در build، دایرکتیو use server پیادهسازی تابع را از باندل کلاینت حذف میکند و جایش یک reference رمزنگاریشده (action ID) میگذارد که با POST صدا زده میشود. یعنی کد سمت سرور لو نمیرود، ولی مسیر برای هر کسی که همان POST را بفرستد باز است.
سه محافظت فریمورک که باید بشناسی:
- CSRF check: هدر
OriginباHost(یاX-Forwarded-Host) مقایسه میشود و مغایرت رد میشود؛ برای پروکسی و CDN ازserverActions.allowedOriginsاستفاده کن. - Body size limit: بدنهی درخواست اکشن بهطور پیشفرض ۱ مگابایت محدود است؛ با
serverActions.bodySizeLimitتغییرش بده. - Encryption key: متغیرهای بستهشده در اکشن inline رمز میشوند؛ برای self-hosting چنداینستانس،
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYرا ثابت نگه دار.
نکتهای که گوگلمنبع نباید جا بماند: این محافظتها جای بررسی سطح اپلیکیشن را نمیگیرند. داکیومنت صریح است — داخل هر اکشن باید auth و authorization چک شود (چون «فقط فرم را در صفحه لاگینشده رندر کردم» مرز امنیتی نیست؛ POST را میشود بدون UI فرستاد)، ورودیها با اسکیما ولیدیت شوند، و خروجی به اندازهای که UI لازم دارد محدود شود نه رکورد خام دیتابیس. برای لایه کاملتر، الگوی Data Access Layer با server-only و DTO در راهنمای data security رسمی است — و ما الگوی پایهاش را در مرز use client و RSC هم باز کردهایم.
الگوی درست برای دسترسی به رکورد: کلاینت فقط «تغییر» را بفرستد (آیدی + مقدار جدید)، بقیه از سشن دوباره خوانده شود. مثال رسمی خود داکیومنت:
تلهای که فقط بعد از دیپلوی میبینی: Failed to find Server Action
هر Server Action با یک action ID شناخته میشود که جزو آرتیفکت بیلد است. هر دیپلوی جدید معمولاً IDهای جدید تولید میکند (Next.js حتی وقتی سورس عوض نشده، هر ۱۴ روز یکبار IDها را میچرخاند). نتیجه: کاربری که تب قدیمی را باز نگه داشته، وقتی فرم را سابمیت میکند به اکشنی POST میزند که دیگر وجود ندارد — خطا دقیقاً این است: «Failed to find Server Action».
کارهای موثر طبق داکیومنت deployment considerations:
- rolling deploy بهجای cut-over ناگهانی، مخصوصاً وقتی کاربر وسط یک mutation است
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYرا بین اینستانسها ثابت نگه دار وگرنه referenceهای اکشن دیکریپت نمیشوند- در UI این خطا را بهعنوان مسیر retry نشان بده نه فیل سخت — یک رفرش کاربر را نجات میدهد
یک رفتار که طراحی UI را عوض میکند: دیسپچ ترتیبی
اکشنهای هر کلاینت یکییکی اجرا میشوند؛ اگر کاربر سه اکشن پشتهم بزند، دومی منتظر اولی میماند. این یعنی Promise.all روی اکشنها از سمت کلاینت هیچ فایدهای ندارد — کار موازی را داخل یک اکشن انجام بده یا از Server Component با fetch موازی. و اگر نیاز داری درخواستهای غیر-mutation موازی داشته باشی، داکیومنت صریحاً Route Handler را پیشنهاد میدهد — دقیقاً همان نقطهای که تفکیک «اکشن یا Route» دوباره معنا پیدا میکند.
مقایسه تصمیم:
| نیاز | انتخاب درست | چرا | |---|---|---| | mutation از فرم خود اپ | Server Action | roundtrip واحد: اکشن + revalidate + رندر مجدد | | endpoint عمومی برای کلاینت خارجی (موبایل، webhook) | Route Handler | URL قابل شناخت و قرارداد REST | | چند درخواست موازی از کلاینت | Route Handler یا یک اکشن واحد | اکشنها ترتیبی dispatch میشوند | | خواندن داده برای رندر | Server Component مستقیم | رفتوبرگشت HTTP اضافی Route Handler فقط کندتر است |
چکلیست بازبینی اکشنها
- [ ] هر اکشن داخل خودش auth و ownership چک میکند، نه فقط رندر شرطی فرم
- [ ] ورودیها با اسکیمای سمت سرور ولیدیت میشوند (الگوی کامل در اعتبارسنجی با Zod)
- [ ] خروجی اکشن فقط فیلدهایی است که UI رندر میکند
- [ ]
serverActions.allowedOriginsفقط برای دامنههای پروکسی/CDN واقعاً لازم ست شده - [ ] برای self-hosting،
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYبین اینستانسها یکسان است - [ ] خطای Failed to find Server Action در UI به retry تبدیل شده، نه پیام خطای خام
- [ ] برای آپلود بزرگ،
bodySizeLimitبالا بردهای یا فایل مستقیم به storage رفته
سوالات پرتکرار
Q: Server Action جایگزین کامل API است؟
نه. اکشن برای mutation داخلی از فرمهای خود اپ طراحی شده و URL ندارند — مسیرشان با ID رمزنگاریشده ساخته میشود و از مرورگر قابل صدا زدن مستقیم نیست. هر جا کلاینت بیرونی، webhook، یا درخواست موازی داری، Route Handler لازم است؛ Next.js در راهنمای Backend for Frontend همین تفکیک را رسمی کرده.
Q: چرا بعد از آپدیت، کاربران قدیمی خطا میگیرند؟
چون action IDها با هر بیلد عوض میشوند (حداکثر هر ۱۴ روز حتی بدون تغییر کد) و اکشنی که بیلد قبلی صدا میزد دیگر روی سرور نیست. راه حل: rolling deploy، کلید رمزنگاری ثابت، و مسیر retry در UI.
Q: محافظتهای CSRF فریمورک کافی است؟
برای شروع بله — چک Origin/Host و کوکیهای SameSite بخش بزرگی از ریسک را میبندد. ولی داکیومنت صریحاً میگوید اینها جای چک اپلیکیشنی را نمیگیرند: authorization، ولیدیشن ورودی و فیلتر خروجی باید داخل خود اکشن باشد.