এই পর্বে যা শিখবেন
"use server"লিখলে build-এ কী বদলায়, আর network tab-এ একটি action দেখতে কেমন- JavaScript ছাড়াও কাজ করা form,
useActionState-এ server-এর field error আরuseOptimistic-এর rollback - Mutation-এর পরে
revalidatePath,updateTag,refreshআরredirectকোথায় বসে - নিজের ল্যাবে UI ছাড়া action ডেকে দেখা, কেন action-এর ভেতরের যাচাই ছাড়া সব খোলা
- Server Action, route handler আর tRPC-র মধ্যে কোনটি কখন
১ · Server Action কী — form থেকে সরাসরি server-এর function
একটি রেস্তোরাঁর কথা ভাবুন। আগে ওয়েটার আপনার অর্ডার শুনে একটি স্লিপ লিখতেন, রান্নাঘরের জানালায় দিতেন, রাঁধুনি রান্না করে প্লেট ফেরত পাঠাতেন। এখন ভাবুন টেবিলে একটি বোতাম আছে, যাতে লেখা “এক প্লেট খিচুড়ি”। চাপলেই অর্ডার রান্নাঘরে চলে যায়। স্লিপ লেখা, জানালা খোঁজা, কোন রাঁধুনি এই খাবার বানান তা মনে রাখা, এই কাজগুলো এখন রেস্তোরাঁর ব্যবস্থার দায়িত্ব।
Express-এ এতদিন আপনি নিজে স্লিপ লিখেছেন: একটি route (POST /api/cart), body parse, তারপর React-এ fetch. Next.js-এর Server Action সেই স্লিপের কাজটি framework-কে দেয়। আপনি server-এ একটি async function লেখেন, ওপরে "use server" লেখেন, তারপর সেটি সরাসরি <form action={…}>-এ বা একটি click handler-এ দেন। Route-এর নাম, URL আর body-র format Next.js নিজে ঠিক করে।
'use server' // every export of this file becomes a Server Action
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
import { currentUser, rateLimit } from '@/lib/auth'
export async function addToCart(productId: string) {
const user = await currentUser() // who is calling? read from the cookie
if (!user) return { ok: false, message: 'আগে লগইন করুন' }
if (!rateLimit('cart:' + user.id)) return { ok: false, message: 'একটু পরে চেষ্টা করুন' }
const p = db.products.find((x) => x.id === String(productId))
if (!p || p.stock < 1) return { ok: false, message: 'স্টক নেই' }
p.stock -= 1
db.carts.set(user.id, [...(db.carts.get(user.id) ?? []), p.id])
revalidatePath('/') // re-render the shop page in the same response
return { ok: true, message: 'ঝুড়িতে যোগ হলো' }
}পুরো ফাইলের মাথায় 'use server' লিখলে প্রতিটি export একটি action. আরেক পথ হলো একটি Server Component-এর ভেতরে inline function লিখে তার প্রথম লাইনে 'use server' দেওয়া। Client Component-এর ভেতরে action বানানো যায় না, শুধু আলাদা ফাইল থেকে import করে ডাকা যায়। কোন component কোথায় চলে, তা পর্ব ৫২-এ (Server Component আর Client Component) আছে। এখানে শুধু মনে রাখুন: 'use client' একটি সীমানা, যার ওপারে কোড browser-এ যায়। 'use server' উল্টো দিকের সীমানা, যার ওপারের কোড কখনো browser-এ যায় না, যায় শুধু একটি ঠিকানা।
Server Action দেখতে function call, কিন্তু আসলে একটি HTTP POST. Function-এর argument যায় request body-তে, আর return মান আসে response-এ। মাঝখানের নেটওয়ার্ক অদৃশ্য হলেও সেখানে আছে, তাই নেটওয়ার্কের সব নিয়ম এখানেও খাটে: ধীর হতে পারে, ব্যর্থ হতে পারে, আর অন্য কেউও একই POST পাঠাতে পারে।
নামটি নিয়ে একটু বিভ্রান্তি আছে। Next.js মে ২০২৩-এ সংস্করণ 13.4-এ Server Action পরীক্ষামূলকভাবে আনে, আর অক্টোবর ২০২৩-এ Next.js 14-এ এটি stable হয়। ভিত্তিটি React-এর, আর ডিসেম্বর ২০২৪-এ React 19 stable হয়। React-এর docs এখন সাধারণ নামটি বলে Server Function; সেই function form-এর action-এ বা transition-এর ভেতরে ডাকা হলে তাকে বলে Server Action. এই পর্বে এই দুই নাম সেই অর্থেই আসবে। লেখার সময়ে ল্যাবের version: Next.js 16.4.4, React 19.3.1, Zod 4.6.7, TypeScript 6.0.3.
এই পর্বের সব Next.js আচরণ একটি ছোট সত্যিকারের project থেকে, যার নাম ~/bazarghar. ডেটা একটি in-memory module-এ, login একটি নকল cookie (sid-rahim-example, sid-admin-example), server বসেছে শুধু 127.0.0.1-এ। next build-এর সত্যিকারের ফল:
~/bazarghar$ npx next build
▲ Next.js 16.4.4 (Turbopack)
✓ Compiled successfully in 158ms
Finished TypeScript in 1163ms ...
✓ Generating static pages using 7 workers (6/6) in 435ms
Route (app)
┌ ƒ /
├ ○ /_not-found
├ ƒ /admin
├ ○ /checkout
└ ○ /checkout/done
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand
/ আর /admin cookie পড়ে, তাই ƒ (প্রতি request-এ render). /checkout পাতাটি static (○), অথচ তার form একটি server function ডাকে। অর্থাৎ পাতা static হলেও action সব সময় request-এর সময় server-এ চলে। Rendering-এর চিহ্নগুলোর পূর্ণ মানে পর্ব ৫৫-এ আসবে, আর মানচিত্রটি দেখেছেন পর্ব ৩-এ (Rendering-এর মানচিত্র).
২ · Network tab-এ একটি action — POST, action ID আর উত্তর
“Function ডাকলাম” কথাটি সুন্দর, কিন্তু পরীক্ষা ছাড়া বিশ্বাস করবেন না। বাজারঘরের দোকানের পাতায় একটি “ঝুড়িতে দিন” বোতাম আছে, যা addToCart('p1') ডাকে। Edge-এ Playwright দিয়ে বোতামটি চেপে request-এর header ধরা হলো। DevTools-এর Network tab খুললে আপনিও ঠিক এই সারিটি দেখবেন (cookie আর কিছু browser header বাদ দেওয়া):
# সত্যিকারের run: Edge, next start, ক্লিক “ঝুড়িতে দিন”
POST http://127.0.0.1:3547/
accept: text/x-component
content-type: text/plain;charset=UTF-8
next-action: 40998758fd943d2d9695574c1db2516544439ca2ca
next-router-state-tree: %5B%22%22%2C%7B%22children%22%3A…
origin: http://127.0.0.1:3547
sec-fetch-site: same-origin
BODY: ["p1"]
STATUS 200 text/x-component
চারটি জিনিস খেয়াল করুন। URL হলো পাতারই URL (/), আলাদা কোনো /api নেই। কোন function চলবে, তা বলে next-action header-এর লম্বা hex মান, যাকে বলে action ID. Argument গেছে একটি JSON array হিসেবে: ["p1"]. আর উত্তরের ধরন text/x-component, যা React-এর নিজস্ব stream format.
এই ID কোথা থেকে এলো? Build-এর সময় compiler action-এর আসল কোড server-এ রাখে, আর client bundle-এ বসায় একটি ছোট প্রতিনিধি। নিজের build-এর ফাইলেই দেখা যায়:
~/bazarghar$ cat .next/server/server-reference-manifest.json # অংশ
"40998758fd943d2d9695574c1db2516544439ca2ca": {
"workers": { "app/admin/page": {…}, "app/page": {…} },
"filename": "app/actions.ts",
"exportedName": "addToCart"
}
~/bazarghar$ grep -o "createServerReference(\"4099[^)]*)" .next/static/chunks/*.js
createServerReference("40998758fd943d2d9695574c1db2516544439ca2ca",n.callServer,void 0,n.findSourceMapURL,"addToCart")
Browser-এর chunk-এ আছে ID, একটি dispatcher (callServer) আর function-এর নাম, কিন্তু currentUser, db বা rateLimit-এর একটি অক্ষরও নেই। Manifest server-কে বলে কোন ID কোন ফাইলের কোন export. Next.js-এর docs অনুযায়ী এই ID build-এর সময় তৈরি encrypted, অনুমান-অযোগ্য মান, নতুন build-এ বদলায় (build cache থাকলেও বড়জোর ১৪ দিনে)। তাই পুরোনো ট্যাব খোলা রেখে নতুন deploy হলে “Failed to find Server Action” error আসতে পারে।
একটি Server Action-এর পুরো পথ
Browser → POST with Next-Action → framework checks → your checks → one response
৩ · JavaScript ছাড়াও চলে — progressive enhancement
ধরুন রাজশাহীর একজন ক্রেতা দুর্বল নেটওয়ার্কে বাজারঘরের checkout খুলেছেন। HTML এসে গেছে, কিন্তু JavaScript এখনো নামছে। পুরোনো SPA-তে এই কয়েক সেকেন্ড form মৃত, কারণ submit চালায় JavaScript. Server Action-এর form হলো একটি আসল HTML <form method="POST">, তাই browser নিজেই পাঠাতে পারে। আগে সাধারণ HTML কাজ করে, JavaScript এলে অভিজ্ঞতা আরও মসৃণ হয়। এই নকশার নাম progressive enhancement.
বাজারঘরের ঠিকানা form একটি Client Component, যেখানে useActionState(saveAddress, {}). Edge-এ দুবার চালানো হলো: একবার JavaScript চালু, একবার পুরো বন্ধ (javaScriptEnabled: false). প্রথমবার ভুল ডেটা, দ্বিতীয়বার ঠিক ডেটা।
# সত্যিকারের run ১: JavaScript বন্ধ (Edge)
REQ POST /checkout | next-action: None | content-type: multipart/form-data
body: … name="$ACTION_REF_1" … name="$ACTION_1:0" {"id":"602092a114bef5…","bound":"$@1"} …
RES 200 /checkout | content-type: text/html; charset=utf-8
ERRORS {'name': 'নাম অন্তত ২ অক্ষর', 'phone': 'ফোন নম্বর ঠিক নয়', 'district': 'জেলা বাছুন', 'line': 'ঠিকানা আরও লিখুন'}
phone value kept: 0171
REQ POST /checkout | next-action: None | content-type: multipart/form-data
RES 303 /checkout | location: /checkout/done
URL /checkout/done | h1: ঠিকানা জমা হয়েছে
# সত্যিকারের run ২: JavaScript চালু (Edge)
REQ POST /checkout | next-action: 602092a114bef57827ec08672542a0850c312b4531 | content-type: multipart/form-data
RES 200 /checkout | content-type: text/x-component
ERRORS {… একই চারটি বার্তা …}
REQ POST /checkout | next-action: 602092a114bef57827ec08672542a0850c312b4531
RES 200 /checkout | content-type: text/x-component
URL /checkout/done | h1: ঠিকানা জমা হয়েছে
দুই run-এর ফল একই, পথ আলাদা। JavaScript বন্ধ থাকলে next-action header নেই। তার বদলে server-render করা form-এর ভেতরে লুকানো $ACTION_REF_1 আর $ACTION_1:0 field ছিল, তাতেই action ID. উত্তর এসেছে পুরো HTML পাতা হিসেবে, যেখানে error বসানো, আর ফোনের ঘরে 0171 রয়ে গেছে কারণ action ভুলের সাথে values-ও ফেরত দিয়েছে। সফল হলে server সাধারণ HTTP 303 redirect দিয়েছে। JavaScript চালু থাকলে একই action, কিন্তু উত্তর text/x-component stream, আর redirect-টিও সেই stream-এর ভেতর দিয়ে client router করেছে, তাই 303 দেখা যায়নি।
এই সুবিধা নিজে থেকে সব জায়গায় আসে না। onClick-এ ডাকা action, বা <form action>-এ দেওয়া একটি client-side function, JavaScript ছাড়া কিছুই করে না। বাজারঘরের “ঝুড়িতে দিন” বোতামটি ঠিক এমন (এর ভেতরে useOptimistic আছে), তাই সেটি JavaScript-নির্ভর। কোন কাজ JavaScript ছাড়াও চলা জরুরি, তা নকশার সময়ই বেছে নিন। Checkout, login, যোগাযোগ form প্রায়ই এই তালিকায় পড়ে।
Next.js-এর docs আরও একটি সূক্ষ্ম কথা বলে: Client Component-এর form-এ JavaScript লোড হওয়ার আগে submit হলে React সেই submit সারিতে রাখে আর hydration-এ সেই অংশকে আগে নেয়। Server Component-এর form-এ JavaScript না এলেও সরাসরি পাঠানো যায়। Hydration-এর খুঁটিনাটি পর্ব ৫৬-এ। আর useActionState-এর তৃতীয় argument permalink লাগে তখন, যখন JavaScript আসার আগে জমা দেওয়া form-কে অন্য URL-এর পাতায় পৌঁছাতে হয়; এক পাতার form-এ সাধারণত লাগে না।
৪ · useActionState আর Zod — server থেকে field error ফেরানো
পর্ব ১২-এ (React-এর form) React 19-এর useActionState আর useFormStatus শিখেছেন: function পায় (prevState, formData), ফেরত মান হয় নতুন state, আর form-এর ভেতরের বোতাম pending জানে। Server Action-এ নতুন কিছু শিখতে হয় না, শুধু function-টি এখন server-এ। তাই একটি প্রশ্নের উত্তর জরুরি: ভুল হলে action কী ফেরত দেবে?
নিয়মটি পর্ব ২৮-এর (Error handling) মতোই। প্রত্যাশিত ভুল, যেমন ভুল ফোন নম্বর, হলো ফল, তাই ফেরত দিন। অপ্রত্যাশিত ভুল, যেমন database বন্ধ, হলো exception, তাই ছুড়ুন, যা error.js ধরবে (অংশ ৯)। প্রথমে নিচের লাইভ বাক্সটি দেখুন। এখানে একটি সাধারণ async function server-এর ভূমিকায় অভিনয় করছে, Next.js নেই, এটি একটি শিক্ষণ মডেল। বাক্সটি শুরুতে ভুল মান নিয়ে খোলে। “জমা দিন” চাপুন, তারপর ঠিক নাম আর নম্বর লিখে আবার চাপুন।
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const wait = ms => new Promise(r => setTimeout(r, ms));
// teaching model: this async function plays the Server Action
async function saveAddress(prev, formData) {
await wait(300);
const v = Object.fromEntries(formData), errors = {};
if (v.name.trim().length < 2) errors.name = 'নাম অন্তত ২ অক্ষর';
if (!/^01[3-9]\d{8}$/.test(v.phone.trim())) errors.phone = 'ফোন নম্বর ঠিক নয়';
console.log('server got', JSON.stringify(v), Object.keys(errors).length ? 'errors' : 'ok');
return Object.keys(errors).length ? { errors, values: v } : { done: true, values: {} };
}
function Field({ name, label, state }) {
return <p><label>{label} <input name={name} defaultValue={state.values?.[name]} /></label>
<small style={{ color: 'crimson' }}> {state.errors?.[name]}</small></p>;
}
function Checkout() {
const [state, action, pending] = useActionState(saveAddress, { values: { name: 'R', phone: '0171' } });
console.log('render pending =', pending, 'errors =', JSON.stringify(state.errors ?? null));
return <form action={action}>
<Field name="name" label="নাম" state={state} />
<Field name="phone" label="ফোন" state={state} />
<button disabled={pending}>{pending ? 'জমা হচ্ছে…' : 'জমা দিন'}</button>
{state.done && <p role="status">ঠিকানা জমা হয়েছে</p>}
</form>;
}
createRoot(document.getElementById('root')).render(<Checkout />);
</script>খোলার সময় একটি লাইন: render pending = false errors = null. Edge-এ Playwright দিয়ে চাপার পরে ক্রমটি ছিল: render pending = true errors = null, তারপর server got {"name":"R","phone":"0171"} errors, তারপর render pending = false errors = {"name":"নাম অন্তত ২ অক্ষর",. ঠিক মান দিয়ে দ্বিতীয়বার: server got {"name":"Rahim","phone":"01712345678"} ok, শেষে errors = null আর পর্দায় “ঠিকানা জমা হয়েছে”। লক্ষ করুন, error-এর সাথে values ফেরত না দিলে React-এর form reset ব্যবহারকারীর লেখা মুছে দিত; পর্ব ১২-এর সতর্কতাটিই এখানে।
Server-এ আসল যাচাই: Zod-এর safeParse
নিয়মগুলো হাতে if দিয়ে না লিখে একটি schema-তে রাখুন। পর্ব ২১-এ একই Zod schema browser আর server দুই জায়গায় চালিয়েছেন, আর পর্ব ২৭-এ Express-এ schema দিয়ে request যাচাই করেছেন। Server Action-এ পদ্ধতি একই: FormData থেকে মান তুলুন, safeParse চালান, ব্যর্থ হলে z.flattenError দিয়ে field error-এর একটি object বানান।
const Address = z.object({
name: z.string().trim().min(2, 'নাম অন্তত ২ অক্ষর'),
phone: z.string().trim().regex(/^01[3-9]\d{8}$/, 'ফোন নম্বর ঠিক নয়'),
district: z.enum(['Dhaka', 'Rajshahi', 'Khulna'], { error: 'জেলা বাছুন' }),
line: z.string().trim().min(5, 'ঠিকানা আরও লিখুন').max(200),
})
export async function saveAddress(prev: AddressState, formData: FormData): Promise<AddressState> {
const user = await currentUser()
if (!user) return { errors: { form: ['আগে লগইন করুন'] } }
const raw = Object.fromEntries(['name', 'phone', 'district', 'line']
.map((k) => [k, String(formData.get(k) ?? '')]))
const parsed = Address.safeParse(raw)
if (!parsed.success) return { errors: z.flattenError(parsed.error).fieldErrors, values: raw }
db.addresses.push({ userId: user.id, ...parsed.data }) // userId from the session, never the form
redirect('/checkout/done')
}Zod-এর ভেতরে আসলে কী ঘটে? নিচের ছোট শিক্ষণ মডেলটি একই আকারের ফল দেয়। প্রতিটি field-এর একটি নিয়ম, নিয়ম ভাঙলে বার্তা, আর schema-তে নেই এমন key বাদ।
// A tiny Zod-like teaching model: each rule returns an error message or null.
const rules = {
name: (v) => (v.length >= 2 ? null : 'নাম অন্তত ২ অক্ষর'),
phone: (v) => (/^01[3-9]\d{8}$/.test(v) ? null : 'ফোন নম্বর ঠিক নয়'),
district: (v) => (['Dhaka', 'Rajshahi', 'Khulna'].includes(v) ? null : 'জেলা বাছুন'),
line: (v) => (v.length >= 5 && v.length <= 200 ? null : 'ঠিকানা আরও লিখুন'),
};
function safeParse(input) {
const fieldErrors = {};
const data = {};
for (const [key, check] of Object.entries(rules)) {
const value = typeof input[key] === 'string' ? input[key].trim() : '';
const error = check(value);
if (error) fieldErrors[key] = [error];
else data[key] = value;
}
// keys without a rule (a forged "price", say) never reach data
return Object.keys(fieldErrors).length
? { success: false, fieldErrors }
: { success: true, data };
}
const tries = [
{ name: 'R', phone: '0171', line: 'abc' },
{ name: ' Rahim ', phone: '01712345678', district: 'Rajshahi', line: 'House 12, Road 3', price: '1' },
];
for (const t of tries) console.log(JSON.stringify(safeParse(t)));প্রথম লাইন: {"success":false,"fieldErrors":{"name":["নাম অন্তত ২ অক্ষর"],. দ্বিতীয়: {"success":true,"data":{"name":"Rahim",. নামের দুই পাশের ফাঁকা কেটে গেছে, আর জাল price key ফলেই নেই। একই দুই input আসল Zod 4.6.7-এর Address schema আর z.flattenError-এ চালিয়ে হুবহু একই দুই লাইন পাওয়া গেছে, কারণ z.object অজানা key ডিফল্টে ফেলে দেয়। প্রতিটি মান array, কারণ একটি field একাধিক নিয়ম ভাঙতে পারে; form সাধারণত প্রথমটি দেখায়।
দুটি ছোট কিন্তু জরুরি অভ্যাস। প্রথম, userId আসে session থেকে, form থেকে নয়; form-এ লুকানো userId ঘর থাকলেও সেটি অগ্রাহ্য করুন। দ্বিতীয়, ফেরত মান ছোট রাখুন। Action-এর return serialize হয়ে browser-এ যায়, তাই পুরো database সারি নয়, শুধু UI-র যা লাগে। import 'server-only' দিয়ে database আর session-এর ফাইল চিহ্নিত রাখলে ভুল করে Client Component-এ import করলে build-ই ব্যর্থ হয়।
৫ · Mutation-এর পরে — revalidate, redirect আর একটি roundtrip
ডেটা বদলানোর পরে পর্দা পুরোনো থাকলে ব্যবহারকারী ভাবেন কাজ হয়নি, আবার চাপেন। তাই action-এর শেষে বলে দিতে হয় কোন cache পুরোনো হলো। পর্ব ১৯-এ TanStack Query-তে এ কাজ ছিল invalidateQueries. Next.js-এ server-এর cache আর client router দুটোই আছে, আর cache-এর পুরো গল্প পর্ব ৫৩-এ (Data fetching আর cache). এখানে শুধু action থেকে কোন ডাক কখন:
| ডাক | কী করে | Action-এর উত্তরেই নতুন পর্দা? |
|---|---|---|
revalidatePath('/') | একটি path-এর cache বাতিল | হ্যাঁ |
updateTag('cart') | Tag সঙ্গে সঙ্গে বাতিল; নিজের লেখা নিজে পড়া। শুধু Server Action-এ | হ্যাঁ, নতুন ডেটার অপেক্ষা করে |
revalidateTag('cart', 'max') | Stale-while-revalidate: পরে পেছনে নতুন করে | না, পরের পড়ায় দেখা যায় |
refresh() | Cache না ছুঁয়ে বর্তমান route আবার আনে | হ্যাঁ |
redirect('/x') | Control-flow exception ছুড়ে অন্য পাতায় | হ্যাঁ, গন্তব্যের payload সহ |
Next.js 16-এ updateTag আর refresh নতুন, আর revalidateTag এখন একটি দ্বিতীয় argument (cache-life profile) চায়। পুরোনো টিউটোরিয়ালে এক argument-এর revalidateTag('x') দেখলে সেটি আগের যুগের। এই নামগুলো আপনার version-এর docs-এ মিলিয়ে নিন।
সবচেয়ে সুন্দর অংশ হলো roundtrip-এর সংখ্যা। revalidatePath ডাকলে Next.js action চালায়, তারপর একই request-এর ভেতরে পাতাটি আবার render করে, আর উত্তরে দুটিই পাঠায়। ল্যাবে মাপা হলো: লগইন ছাড়া addToCart-এর উত্তর (কোনো revalidate নেই) ছিল 2 লাইন, ১২২ byte. রহিমের সফল ডাকে (revalidatePath('/') সহ) উত্তর 22 লাইন, যার ভেতরে ফেরত মান আর নতুন পাতার গাছ দুটোই। ধাপে ধাপে দেখুন:
- ক্লিক
addToCart('p1')browser - পাহারা
Origin, sizeframework - Action
stock 3 → 2আপনার কোড - Re-render
revalidatePathএকই request - নতুন পর্দা
ঝুড়িতে: 1একটি উত্তর
POST / next-action: 40998758fd94… origin: http://127.0.0.1:3547 cookie: sid=sid-rahim-example body: ["p1"]
origin host 127.0.0.1:3547 host 127.0.0.1:3547 মিলেছে body 6 byte ≤ 1 MB ঠিক আছে action ID manifest-এ আছে → addToCart
[addToCart] p1 user = u1 p1.stock 3 → 2 cart(u1) [] → ["p1"] return { ok: true, message: 'ঝুড়িতে যোগ হলো' }
Home() আবার চলে
count = carts.get('u1').length = 1
p1 stock = 2
নতুন RSC payload তৈরি
0:{"a":"$@1","q":"","i":false,"t":{…নতুন গাছ…}}
1:{"ok":true,"message":"ঝুড়িতে যোগ হলো"}
9:[…"p",null,{"id":"cart","children":["ঝুড়িতে: ",1]}…
…"Miniket Rice 5kg"," — ৳",450," — stock ",2…]
- ক্লিক। Client-এর প্রতিনিধি function argument-কে
["p1"]বানিয়ে পাতার URL-এ POST করে, header-এ action ID. Browser নিজে cookie আর Origin জুড়ে দেয়। - Framework-এর পাহারা। Next.js Origin-এর host আর নিজের host মেলায়, body-র আকার ১ MB-র নিচে কি না দেখে, তারপর manifest-এ ID খুঁজে
addToCartপায়। এখানে কেউ “রহিম কে” প্রশ্ন করেনি। - আপনার action. Cookie থেকে
u1পাওয়া গেল, rate limit পার, p1-এর stock ৩ থেকে ২, রহিমের ঝুড়িতে একটি পণ্য। ফেরত মান তৈরি, কিন্তু উত্তর এখনো যায়নি। - Re-render.
revalidatePath('/')বলেছে দোকানের পাতা পুরোনো। Next.js একই request-এHome()আবার চালায়: ঝুড়িতে ১, stock ২। - একটি উত্তর। লাইন
1:-এ ফেরত মান, বাকি লাইনে নতুন পাতা। Client দুটোই একবারে বসায়, আলাদা কোনো GET লাগে না।
Redirect: try/catch-এর বাইরে, revalidate-এর পরে
redirect() আসলে একটি বিশেষ error ছোড়ে, যা Next.js ধরে route বদলায়। তাই এর পরের কোনো লাইন চলে না। দুটি নিয়ম এখান থেকে আসে। এক, গন্তব্য পাতায় নতুন ডেটা লাগলে revalidatePath আগে ডাকুন। দুই, redirect-কে নিজের try { … } catch-এর ভেতরে রাখবেন না, নইলে আপনার catch সেই বিশেষ error গিলে ফেলবে আর redirect হবে না। saveAddress-এ তাই redirect একেবারে শেষ লাইনে, কোনো try-র বাইরে।
৬ · useOptimistic — আগে দেখান, ব্যর্থ হলে ফেরান
বিকাশে টাকা পাঠালে অ্যাপ সঙ্গে সঙ্গে “প্রক্রিয়াধীন” দেখায়, নিশ্চিত হলে সবুজ টিক, ব্যর্থ হলে টাকা ফেরত। Optimistic update-এর ভাবনা একই: server-এর উত্তরের অপেক্ষা না করে পর্দা আগেই বদলান, তারপর সত্যি মিলিয়ে নিন। পর্ব ১৯-এ TanStack Query দিয়ে এটি হাতে করেছেন। React 19-এ এর জন্য একটি hook আছে: useOptimistic(state, update). এটি একটি অস্থায়ী মান দেয়, যা চলমান action-এর সময় টিকে থাকে; action শেষ হলে React আবার আসল state দেখায়।
নিচের বাক্সে server-এ stock মাত্র ১টি। প্রথম ক্লিক সফল, দ্বিতীয়টি ব্যর্থ হবে। এটিও শিক্ষণ মডেল; addToCart এখানে একটি স্থানীয় async function.
<div id="root"></div>
<script src="../course-kit/vendor/react-lab.js"></script>
<script type="text/jsx">
const wait = ms => new Promise(r => setTimeout(r, ms));
let stock = 1; // teaching model of the server's stock
async function addToCart() {
await wait(400);
if (stock < 1) throw new Error('স্টক নেই');
stock -= 1;
}
function Cart() {
const [count, setCount] = useState(0); // what the server confirmed
const [shown, addShown] = useOptimistic(count, (c, n) => c + n);
const [msg, setMsg] = useState('');
console.log('render count =', count, 'shown =', shown, 'msg =', msg || '-');
async function add() {
addShown(1); // show it at once
try {
await addToCart();
startTransition(() => { setCount(c => c + 1); setMsg('যোগ হলো'); });
} catch (e) {
startTransition(() => setMsg(e.message)); // shown falls back to count
}
}
return <form action={add}>
<button>ঝুড়িতে দিন</button> <b>ঝুড়িতে: {shown}</b> <span role="status">{msg}</span>
</form>;
}
createRoot(document.getElementById('root')).render(<Cart />);
</script>খোলার সময়: render count = 0 shown = 0 msg = -. Edge-এ দুবার চাপা হলো। প্রথম ক্লিকে সঙ্গে সঙ্গে shown = 1 (count তখনো ০), ৪০০ ms পরে count = 1 shown = 1 msg = যোগ হলো. দ্বিতীয় ক্লিকে আবার আগেভাগে shown = 2, তারপর server-এর “স্টক নেই”-এ render count = 1 shown = 1 msg = স্টক নেই. কোনো “ফেরানো” কোড লেখা হয়নি; transition শেষ হতেই অস্থায়ী মান হারিয়ে আসল count দেখা গেছে। setCount কেন startTransition-এ? await-এর পরের state বদল React 19-এ আপনাআপনি transition-এর অংশ নয়; বাইরে থাকলে মাঝের এক মুহূর্তে count ১ আর অস্থায়ী +১ মিলে ২ দেখাতে পারত।
আসল Next.js-এ এই কাজ আরও সরল, কারণ নিশ্চিত মান আসে server থেকে, Server Component-এর prop হয়ে। বাজারঘরের CartButton:
'use client'
import { useOptimistic, useState } from 'react'
import { addToCart } from './actions'
export default function CartButton({ productId, count }: { productId: string; count: number }) {
const [shown, addShown] = useOptimistic(count, (c: number, n: number) => c + n)
const [msg, setMsg] = useState('')
async function add() {
addShown(1) // optimistic +1 while the action runs
const r = await addToCart(productId) // revalidatePath sends the new count as a prop
setMsg(r.message)
}
return (
<form action={add} style={{ display: 'inline' }}>
<button>ঝুড়িতে দিন ({shown})</button> <span role="status">{msg}</span>
</form>
)
}সত্যিকারের run-এ (Edge, next start) Mustard Oil-এর stock ছিল ১। বোতামটি দুবার চাপা হলো, প্রতিবার ৮০০ ms অপেক্ষা। পর্দার প্রতিটি বদল একটি MutationObserver দিয়ে ধরা হয়েছে:
1addShown(1)2const r = await addToCart(id)3setMsg(r.message)4// server: revalidatePath('/')5// server: { ok: false } → no revalidate
| count (server) | |
| shown (বোতাম) | |
| stock | |
| msg |
- শুরু। Server বলেছে ঝুড়িতে ০, stock ১। বোতাম আর ওপরের লেখা দুটোই ০।
- প্রথম ক্লিক, আগাম দেখানো।
addShown(1)বোতামে ১ বসাল। ওপরের “ঝুড়িতে: 0” server-render করা, তাই এখনো ০। POST পথে আছে। - Server রাজি। Stock ১ থেকে ০,
revalidatePath-এর নতুন পাতায় propcount = 1. Transition শেষ, অস্থায়ী মান সরে আসল ১। দুই জায়গাতেই ১। - দ্বিতীয় ক্লিক। আবার আগাম +১, বোতামে ২। ব্যবহারকারী এক মুহূর্তের জন্য ২ দেখেন।
- Rollback. Server ফেরত দিল
{ ok: false, message: 'স্টক নেই' }, কোনো revalidate নেই, তাইcount১-ই। Transition শেষ হতেই বোতাম ২ থেকে ১-এ ফিরল, পাশে “স্টক নেই”। Console-এ দুটি সত্যিকারের লাইন:[client] result {ok: true, …}আর{ok: false, message: স্টক নেই}.
কোথায় optimistic চলবে না? যেখানে ভুল দেখানোর দাম বেশি: পেমেন্ট সফল, অর্ডার নিশ্চিত, stock “আছে”। “লাইক”, ঝুড়ির সংখ্যা, প্রিয় তালিকা, এসব জায়গায় ক্ষণিকের ভুলের ক্ষতি নেই। আর optimistic মান কখনো server-এর সিদ্ধান্ত নয়; শেষ কথা সব সময় action-এর ফেরত মান আর revalidate করা ডেটা।
৭ · প্রতিটি action একটি public endpoint — যাচাই action-এর ভেতরে
এই পর্বের সবচেয়ে জরুরি কথা এটি। অংশ ২-এ দেখেছেন, একটি action মানে একটি POST, যাতে একটি ID. যে কেউ সেই POST পাঠাতে পারেন: curl, একটি স্ক্রিপ্ট, বা ব্রাউজারের console. Next.js-এর docs-এর ভাষায়, প্রতিটি action-কে একটি অবিশ্বস্ত entry point ধরুন। পাতায় login যাচাই থাকলেও সেটি শুধু ঠিক করে কোন UI দেখানো হবে; action একটি আলাদা দরজা।
একটি অফিস ভবনের কথা ভাবুন। মূল ফটকে দারোয়ান পরিচয়পত্র দেখেন (পাতার যাচাই)। কিন্তু ভবনের পেছনে একটি মালপত্রের দরজা আছে, যা সরাসরি হিসাব বিভাগে যায় (action). দারোয়ান সেখানে নেই। হিসাব বিভাগের কর্মকর্তা যদি নিজে প্রত্যেককে জিজ্ঞেস না করেন “আপনি কে, এই কাগজে সই করার অধিকার আছে কি”, তাহলে মূল ফটকের পাহারা অর্থহীন।
বাজারঘরের /admin পাতা শুরুতেই if (user?.role !== 'admin') redirect('/') করে। ল্যাবে দুটি delete action আছে: deleteProduct, যার ভেতরে role যাচাই, আর শুধু ল্যাবের জন্য deleteProductUnsafe, যেখানে কোনো যাচাই নেই। প্রথমে ধারণাটি একটি শিক্ষণ মডেলে দেখুন। নিচের node:http server Next.js নয়, কিন্তু একই আকার মেনে চলে: header-এ action ID, body-তে argument-এর JSON array, আর একটি ID থেকে function-এর তালিকা।
const http = require('node:http');
// Teaching model of an action endpoint. Fake local data only.
const products = ['p1', 'p2', 'p3'];
const sessions = { 'sid-rahim-example': { id: 'u1', role: 'customer' } };
const remove = (id) => products.splice(products.indexOf(id), 1);
const actions = { // action id -> function (like a manifest)
a1: async (user, id) => { remove(id); return 'deleted ' + id; }, // no check
a2: async (user, id) => {
if (user?.role !== 'admin') throw new Error('Forbidden'); // check inside
remove(id); return 'deleted ' + id;
},
};
const server = http.createServer(async (req, res) => {
let body = '';
for await (const chunk of req) body += chunk;
if (new URL(req.headers.origin).host !== req.headers.host) return res.end('403 origin mismatch');
const user = sessions[(req.headers.cookie ?? '').replace('sid=', '')];
try { res.end('200 ' + await actions[req.headers['next-action']](user, ...JSON.parse(body))); }
catch (e) { res.end('500 ' + e.message); }
});
server.listen(0, '127.0.0.1', async () => {
const base = 'http://127.0.0.1:' + server.address().port;
const call = async (label, id, args, extra = {}) => {
const headers = { 'next-action': id, origin: base, ...extra };
const r = await fetch(base, { method: 'POST', headers, body: JSON.stringify(args) });
console.log(label.padEnd(24), await r.text(), '| left:', products.join(','));
};
await call('a1, no login', 'a1', ['p3']);
await call('a2, rahim (customer)', 'a2', ['p2'], { cookie: 'sid=sid-rahim-example' });
await call('a2, other origin', 'a2', ['p2'], { origin: 'http://evil.example' });
server.close();
});তিনটি লাইন। a1, no login 200 deleted p3 | left: p1,p2: login ছাড়াই একটি পণ্য মুছে গেল, কারণ a1-এর ভেতরে কেউ কিছু জিজ্ঞেস করেনি। a2, rahim (customer) 500 Forbidden | left: p1,p2: রহিম লগইন করা, কিন্তু admin নন, তাই action নিজেই থামাল। a2, other origin 403 origin mismatch | left: p1,p2: অন্য origin থেকে আসা request action পর্যন্ত পৌঁছায়ইনি। শেষ পাহারাটি framework-এর, মাঝেরটি আপনার।
এবার আসল Next.js. নিজের ল্যাবের server-এ (127.0.0.1:3547), নিজের build-এর manifest থেকে নেওয়া ID দিয়ে, একটি ছোট node স্ক্রিপ্ট UI ছাড়া চারটি POST পাঠাল। প্রতিটির header ছিল Next-Action: <ID>, Content-Type: text/plain আর ঠিক origin. সত্যিকারের run:
~/bazarghar$ node direct.mjs auth
1 no cookie { status: 200, text: '0:{"a":"$@1",…}\n1:{"ok":false,"message":"আগে লগইন করুন"}\n' }
2 rahim { status: 200, text: '2:"$Sreact.fragment"\n3:"/_next/static/chunks/…' } # নতুন পাতাসহ
3 delete rahim { status: 200, text: '0:{"a":"$@1",…}\n1:E{"digest":"3545386112"}\n' }
4 unsafe none { status: 200, text: '0:{"a":"$@1",…}\n1:"$undefined"\n' }
# একই সময়ে server-এর log
[addToCart] p1 user = none
[addToCart] p1 user = u1
[deleteProduct] p2 role = customer
⨯ Error: Forbidden
at … { digest: '3545386112' }
[deleteProductUnsafe] p2
লাইন ৪-ই পুরো শিক্ষা: কোনো cookie ছাড়া, কোনো পাতা না খুলে, deleteProductUnsafe('p2') চলল আর Mustard Oil মুছে গেল। /admin পাতার redirect এখানে কোনো কাজে আসেনি, কারণ request পাতায় যায়নি, সরাসরি action-এ গেছে। লাইন ৩-এ একই কাজ deleteProduct-এ থেমেছে, কারণ ভেতরে role দেখা হয়। লক্ষ করুন, ছোড়া error-এর আসল বার্তা “Forbidden” client পায়নি, পেয়েছে শুধু একটি digest; বার্তাটি রয়েছে server-এর log-এ, একই digest-সহ। HTTP status দুই ক্ষেত্রেই 200, তাই status দেখে action-এর সাফল্য বিচার করবেন না।
1// Next.js: Origin, size, ID → OK2export async function deleteProduct(id) {3 const user = await currentUser()4 if (user?.role !== 'admin')5 throw new Error('Forbidden')6 db.products = db.products.filter(…)7 revalidatePath('/')8}
| কে ডাকছে | |
| কোন action | |
| user.role | |
| পণ্য |
- যাচাই নেই। Framework-এর তিন পাহারাই পার, কারণ origin ঠিক, body ছোট, ID আসল। ভেতরে কোনো প্রশ্ন নেই, তাই login ছাড়াই p2 মুছে গেল। সত্যিকারের run-এর লাইন ৪।
- যাচাই আছে, ভুল মানুষ। রহিমের cookie আছে, তাই
currentUser()customer দিল। লাইন ৪-৫-এ থামল, ডেটা অক্ষত, client পেল শুধু digest. - ঠিক মানুষ। Admin-এর cookie: যাচাই পার, পণ্য বাদ,
revalidatePath. একই action, একই ID, পার্থক্য শুধু ভেতরের প্রশ্নের উত্তরে। - সমাধান। যাচাইহীন action মুছে ফেলুন, বা ভেতরে একই
currentUserআর role-এর প্রশ্ন বসান। নিয়ম: যে action ডেটা বদলায়, তার প্রথম লাইন “আপনি কে”, দ্বিতীয় লাইন “এই জিনিসটি কি আপনার”।
Role যাচাই যথেষ্ট নয়, ownership-ও লাগে। “রহিম কি customer” প্রশ্নের পরে “এই ঠিকানা কি রহিমের” প্রশ্ন। পর্ব ৪৮-এ (Permission) নিজের ল্যাবে IDOR ঘটিয়ে বন্ধ করেছেন; Server Action-এ ঠিক একই বাগ হয়, যখন action client থেকে পুরো object নেয় আর তার id-তে ভরসা করে। Next.js docs-এর পরামর্শ: client থেকে শুধু ID আর পরিবর্তনটি নিন, বাকি সব session আর database থেকে নিজে পড়ুন, আর query-তে ownerId: user.id শর্ত দিন। এই যাচাইগুলো এক জায়গায় রাখতে একটি Data Access Layer বানান, যা server-only আর যার প্রতিটি function নিজে authorization করে।
৮ · Framework-এর পাহারা — closure encryption, Origin, body আর rate limit
আপনার যাচাই প্রধান, তবে Next.js-ও কয়েকটি দেয়াল তোলে। প্রতিটি দেয়াল নিজের ল্যাবে ধাক্কা দিয়ে দেখা হয়েছে।
Closure-এর মান: encrypt হয়ে browser ঘুরে আসে
Server Component-এর ভেতরে লেখা inline action বাইরের ভ্যারিয়েবল মনে রাখতে পারে; JavaScript-এর এই ক্ষমতার নাম closure (JavaScript-এর closure পর্ব)। বাজারঘরের admin পাতায় প্রতিটি পণ্যের পাশে একটি “margin” বোতাম, যার action কেনা দাম cost মনে রাখে। কেনা দাম ক্রেতার দেখার কথা নয়।
{db.products.map((p) => {
const cost = p.cost // captured by the inline action below
async function checkMargin() {
'use server'
console.log('[checkMargin]', p.id, 'cost =', cost)
}
return <form key={p.id} action={checkMargin}><button>margin</button></form>
})}কিন্তু action তো পরে, আলাদা request-এ চলবে। তখন cost-এর মান কোথা থেকে আসবে? Server মনে রাখে না; মানটি পাতার সাথে browser-এ যায় আর ক্লিকে ফিরে আসে। Admin-এর HTML-এ সত্যিকারের run:
HIDDEN $ACTION_3:0 = {"id":"4087a8854458e7f285a027efecd633b444ccde0dae" …
HIDDEN $ACTION_3:1 = ["$@2"]
HIDDEN $ACTION_3:2 = "3tnI/Q7nYbtCzFvlmWoijUP4RQjQy4G4rz2a2jUiAQUyrngYz9c4cmIaHNDb2I45…
HIDDEN $ACTION_4:2 = "pgyAxkJSL2glIF/Dq5s1OXfCXgqQqQqk3V9LVVGwHYz6QIvbb6HyUp72r9GRT/cR…
"410" in HTML: False | "cost" in HTML: False
দুই পণ্যের জন্য দুটি লম্বা base64 মান, যা encrypt করা bound মান। কেনা দাম 410 HTML-এ কোথাও নেই। Docs অনুযায়ী প্রতিটি build-এ নতুন key তৈরি হয়, তাই এক build-এর action অন্য build-এ চলে না, আর একাধিক server-এ চালালে সবার জন্য একই key দিতে NEXT_SERVER_ (base64, ১৬/২৪/৩২ byte) environment-এ রাখতে হয়। একই docs স্পষ্ট বলে, শুধু encryption-এর ওপর ভরসা করে গোপন মান পাঠাবেন না। ভালো অভ্যাস হলো closure-এ শুধু ID রাখা, আর গোপন মান action-এর ভেতরে database থেকে পড়া।
CSRF: Origin বনাম Host
CSRF-এর তত্ত্ব CSRF আর cookie পর্বে আর পর্ব ৫০-এ (অ্যাপের নিরাপত্তা তালিকা). Action শুধু POST মেনে নেয়, আর আধুনিক browser-এ cookie ডিফল্টে SameSite=Lax-এর মতো আচরণ করে, যা বেশিরভাগ cross-site POST-এ cookie পাঠায় না। এর ওপরে Next.js request-এর Origin header-এর host নিজের host (বা x-forwarded-host)-এর সাথে মেলায়। নিজের ল্যাবে Origin: http://evil.example দিয়ে পাঠানো হলো:
evil origin { status: 500, text: '0:{"a":"$@1",…}\n1:E{"digest":"3473614277"}\n' }
# server log (সত্যিকারের)
`x-forwarded-host` header with value `127.0.0.1:3547` does not match `origin` header
with value `evil.example` from a forwarded Server Actions request. Aborting the action.
⨯ Error: Invalid Server Actions request.
Action-এর ভেতরের console.log এবার ছাপা হয়নি, অর্থাৎ কোড চলেইনি। Proxy বা CDN-এর পেছনে ভিন্ন domain থেকে action আসলে experimental.-এ সেই host যোগ করতে হয়। একটি ফাঁকও জেনে রাখুন: docs অনুযায়ী যে request-এ Origin header-ই নেই, সেটি সতর্কবার্তা দিয়ে পার হয়। ল্যাবে Origin ছাড়া পাঠানো ডাকটি সত্যিই action পর্যন্ত গেছে ([addToCart] p1 user = none). আধুনিক browser POST-এ Origin পাঠায়, তাই এটি মূলত স্ক্রিপ্টের পথ, আর স্ক্রিপ্টের হাতে আপনার ব্যবহারকারীর cookie থাকে না। কিন্তু এই কারণেই action-এর ভেতরের login যাচাই বাদ দেওয়া যায় না।
Body সীমা: ১ MB
Action-এর request body ডিফল্টে ১ MB, অর্থাৎ 1,048,576 byte পর্যন্ত। ল্যাবে 1,048,590 byte-এর একটি body পাঠানো হলো, সীমার চেয়ে মাত্র ১৪ byte বেশি:
bytes 1048590 { status: 500, text: '0:{"a":"$@1",…}\n1:E{"digest":"2494380138"}\n' }
⨯ Error: Body exceeded 1 MB limit.
To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/next-config-js/serverActions#bodysizelimit
statusCode: 413,
Error object-এ statusCode: 413 থাকলেও HTTP উত্তর ছিল 500. সীমা বাড়াতে experimental.. Docs মনে করিয়ে দেয়, সীমা কাঁচা body-র ওপর, তাই multipart-এর boundary আর header-এর জন্য ১০ থেকে ২০ KB বাড়তি জায়গা রাখুন। বড় ফাইল action দিয়ে না পাঠিয়ে পর্ব ৩০-এর stream বা পর্ব ৬৪-এর presigned URL ভালো পথ। এটি পর্ব ২৭-এর Express body limit-এরই আরেক রূপ।
Rate limit: আপনার কাজ
Framework কতবার ডাকা যাবে তা গোনে না। পর্ব ৪৯-এ login-এর rate limit দেখেছেন; action-এও একই দরকার, বিশেষ করে যেগুলো email পাঠায় বা stock কমায়। বাজারঘরের rateLimit একটি সহজ fixed window: প্রতি ব্যবহারকারী ৬০ সেকেন্ডে ৫ বার। Admin-এর cookie দিয়ে সাতবার ডাকা হলো (p1-এর stock ৩):
~/bazarghar$ node direct.mjs rate
1 "message":"ঝুড়িতে যোগ হলো"
2 "message":"ঝুড়িতে যোগ হলো"
3 "message":"ঝুড়িতে যোগ হলো"
4 "message":"স্টক নেই"
5 "message":"স্টক নেই"
6 "message":"একটু পরে চেষ্টা করুন"
7 "message":"একটু পরে চেষ্টা করুন"
৫ পর্যন্ত গোনা হয়েছে, তাই ষষ্ঠ ডাক থেকে থামানো। এই limiter একটি process-এর memory-তে থাকে, তাই কয়েকটি server বা serverless-এ এটি ভাঙে; সেখানে Redis-এর মতো ভাগ করা store লাগে (পর্ব ৪৪, আর তত্ত্ব rate limiting পর্বে)। Fixed window-এর একটি দুর্বলতাও আছে: জানালার শেষ সেকেন্ডে ৫ আর পরের জানালার প্রথম সেকেন্ডে ৫, অর্থাৎ ২ সেকেন্ডে ১০টি ডাক।
| পাহারা | কে দেয় | ল্যাবের প্রমাণ |
|---|---|---|
| শুধু POST, Origin = Host | Next.js | evil.example → action চলেনি |
| Body ≤ ১ MB | Next.js (বদলানো যায়) | 1,048,590 byte → Body exceeded 1 MB limit |
| Closure-এর মান encrypt | Next.js | HTML-এ 410 নেই |
| অব্যবহৃত action বাদ | Next.js (build) | docs অনুযায়ী; manifest-এ শুধু ব্যবহৃত |
| Login, role, ownership | আপনি | Unsafe action মুছল, safe action থামাল |
| Input যাচাই, ফেরত মানের সীমা | আপনি | Zod, ছোট return |
| Rate limit, idempotency | আপনি | ষষ্ঠ ডাক থামল |
৯ · Error, error.js আর কোন পথ কখন — action, route handler, tRPC
অপ্রত্যাশিত ভুলে action error ছুড়লে নিকটতম error.js (বা error.tsx) ধরে, যা একটি Client Component আর React-এর error boundary. বাজারঘরের ল্যাবে brokenAction ইচ্ছা করে এমন একটি বার্তা ছোড়ে, যাতে একটি নকল host আর password=example-secret. Production build-এ admin হিসেবে বোতাম চাপার পরে সত্যিকারের ফল:
# পর্দায় (app/error.tsx)
কিছু একটা ভুল হয়েছে | Minified React error #441; visit https://react.dev/errors/441 … | digest: 3490098926
# server log
⨯ Error: db connection lost: host=10.0.0.5 password=example-secret
digest: '3490098926'
React-এর error code তালিকায় ৪৪১ মানে: “An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details.” অর্থাৎ production-এ আসল বার্তা browser-এ যায় না, যায় শুধু একটি digest, যা দিয়ে আপনি server log-এ আসল লাইন খুঁজে পান। এটিই error masking. তবু গোপন তথ্য error বার্তায় না রাখাই ভালো, কারণ log-ও অনেকে দেখেন। next dev-এ আসল বার্তা দেখা যায়, তাই dev-এ সব ঠিক দেখে production-এ অবাক হবেন না। এ জন্যই প্রত্যাশিত ভুল (ভুল ফোন, stock নেই) ফেরত দেওয়া হয়, ছোড়া নয়: ছুড়লে ব্যবহারকারী পান শুধু “কিছু একটা ভুল হয়েছে”।
Client Component থেকে ডাকা, আর একটির পর একটি
Action form ছাড়াও ডাকা যায়: onClick-এ, বা useEffect-এ, সাধারণত startTransition-এর ভেতরে, যাতে pending জানা যায়। বাজারঘরের admin বোতাম ঠিক তাই করে: onClick={() => start(() => deleteProduct(id))}. Server Component থেকে action prop হিসেবে Client Component-এ পাঠানোও চলে। একটি আচরণ মনে রাখুন: docs অনুযায়ী Next.js একটি client থেকে action একটির পর একটি পাঠায়। তিনটি ক্লিক দ্রুত দিলে দ্বিতীয়টি প্রথমটির শেষের অপেক্ষা করে। তাই action দিয়ে সমান্তরালে ডেটা আনার চেষ্টা (Promise.all) কাজ করে না; পড়ার কাজ Server Component-এ করুন। এর মানে এ-ও নয় যে server-এ action-গুলো সারিতে থাকে; অন্য ট্যাব বা স্ক্রিপ্টের ডাক একসাথে আসতে পারে, তাই stock কমানোর মতো কাজে পর্ব ৪৩-এর transaction এখনো লাগে।
তিন পথের তুলনা
| প্রশ্ন | Server Action | Route handler | tRPC |
|---|---|---|---|
| URL | লুকানো, পাতার URL + ID | আপনার বানানো, যেমন /api/orders | একটি endpoint, procedure-এর নাম |
| HTTP method | শুধু POST | যেকোনো (GET, PUT…) | query GET, mutation POST |
| কে ডাকবে | নিজের Next.js UI | যে কেউ: মোবাইল অ্যাপ, webhook, অন্য server | নিজের TypeScript client |
| JavaScript ছাড়া form | হ্যাঁ | হাতে করলে | না |
| ভালো মানায় | Form, ছোট mutation | Public API, payment webhook, file | বড় typed API, React ছাড়াও client |
| নিরাপত্তা | তিনটিই খোলা endpoint: ভেতরে login, অনুমতি, যাচাই, rate limit | ||
পর্ব ৩৭-এ (Type-safe API) tRPC দেখেছেন; Server Action সেই “server-এর function, type-সহ” ধারণার একটি অংশ framework-এর ভেতরে এনেছে, তবে শুধু Next.js-এর নিজের UI-র জন্য। মোবাইল অ্যাপ বা অন্য কোনো client যদি একই কাজ করতে চায়, তার জন্য পর্ব ৫৭-এর route handler, কারণ action-এর ID বদলায় আর format React-এর নিজস্ব। আর payment gateway-র webhook কখনো action নয়; তার নিজের signature যাচাই লাগে (পর্ব ৬৫)। সম্প্রদায়ে মতভেদ আছে: কেউ পড়া-লেখা সব action-এ নিতে চান, কেউ action শুধু form-এ রাখেন আর বাকি API আলাদা রাখেন। ছোট দলের ছোট অ্যাপে action সময় বাঁচায়; অনেক client থাকলে স্পষ্ট API-ই টেকে।
আরও গভীরে: Action-এর argument আর ফেরত মান যে format-এ যায়, React তাকে “Flight” বলে; লাইনগুলো 0:, 1: আর $@1-এর মতো reference সেটিরই চিহ্ন। Client পাঠায় encodeReply দিয়ে, server পড়ে decodeReply দিয়ে, যা Date, Map, FormData আর Promise-এর মতো জিনিসও বহন করে। এই decode-এর অংশটি আসলে একটি deserializer, যা অচেনা মানুষের পাঠানো bytes পড়ে, তাই এখানে ভুলের দাম খুব বেশি; ডিসেম্বর ২০২৫-এর CVE-2025-55182 ঠিক এই জায়গার ত্রুটি ছিল (ভাবনার প্রশ্ন ৪)। RFC 6454 Origin-এর সংজ্ঞা দেয়, আর Origin বনাম Host মেলানো OWASP-এর CSRF cheat sheet-এর একটি স্বীকৃত কৌশল।
পরীক্ষা ও ইন্টারভিউতে: (১) Server Action কোন HTTP method-এ যায়? শুধু POST, header-এ action ID. (২) পাতায় login যাচাই থাকলে action-এ লাগবে কি? হ্যাঁ, action সরাসরি ডাকা যায়। (৩) redirect কেন try/catch-এর বাইরে? এটি একটি বিশেষ error ছোড়ে, catch গিলে ফেলে। (৪) Validation error ফেরত দেবেন না ছুড়বেন? ফেরত দিন; ছুড়লে production-এ শুধু digest আসে। (৫) Action বনাম route handler? নিজের UI-র form-এ action, বাইরের client বা webhook-এ route handler.
১০ · AI-এর যুগে Server Action
AI খুব সহজে Server Action লেখে, আর কোডটি প্রায়ই চলে। বিপদ হলো, “চলে” আর “নিরাপদ” আলাদা জিনিস। UI দিয়ে পরীক্ষা করলে যাচাইহীন action আর যাচাইসহ action একই রকম দেখায়; পার্থক্য ধরা পড়ে শুধু UI ছাড়া ডাকলে।
| AI-এর ভুল | কেন সমস্যা | যাচাই |
|---|---|---|
| Action-এ login বা role যাচাই নেই, শুধু পাতায় | UI ছাড়া POST-এ সব খোলা | নিজের ল্যাবে cookie ছাড়া action ডাকা |
Form থেকে userId, price নেওয়া | যে কেউ বদলে পাঠাতে পারেন | Session থেকে পরিচয়, database থেকে দাম |
useFormState, experimental_useOptimistic | পুরোনো নাম; এখন useActionState, useOptimistic | react.dev-এ নিজের version |
এক argument-এর revalidateTag('x') | Next.js 16-এ profile argument লাগে | nextjs.org-এর বর্তমান docs, next build |
redirect try/catch-এর ভেতরে | Redirect হয় না, error গিলে যায় | সফল submit-এর পরে URL দেখা |
| পুরো database সারি return | অপ্রয়োজনীয় তথ্য browser-এ | Network tab-এ উত্তর পড়া |
| Closure-এ গোপন মান, “encrypted তো” | Docs নিজেই ভরসা করতে নিষেধ করে | Closure-এ শুধু ID |
AI-কে action লিখতে বললে শর্তগুলো আগেই দিন। নিচের prompt-এ version, যাচাই, নিরাপত্তা আর পরীক্ষা চারটিই আছে।
AI-এর লেখা action হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন, তারপর “→ উত্তর” চাপুন।
প্র ০১PHP-তে <form action="save.php"> লিখলে server-এর কোড সরাসরি চলত। Server Action কি সেই পুরোনো পথে ফেরা?
অনেকটাই, এবং সেটি জেনেবুঝে। ২০০০-এর দশকে PHP আর পরে Rails-এর ওয়েবে form সরাসরি server-এ যেত, server নতুন পাতা ফেরত দিত। কোড সরল ছিল, JavaScript লাগত না, কিন্তু প্রতিটি ক্লিকে পুরো পাতা নতুন করে আসত। ২০১০-এর দশকে SPA এলো: পাতা একবার, তারপর JSON API আর fetch. অভিজ্ঞতা মসৃণ হলো, কিন্তু প্রতিটি form-এর জন্য এখন দুই জায়গায় কোড: একটি API route আর একটি client-এর ডাক, মাঝে একটি চুক্তি, যা ভেঙে যেতে পারে।
Server Action এই দুই যুগ জোড়া দেয়। উপরিভাগে PHP-র মতো: form-এর action-এ server-এর কোড, JavaScript ছাড়াও চলে (এই পর্বের Edge run-এ 303 redirect দেখেছেন)। ভেতরে SPA-র মতো: JavaScript থাকলে পুরো পাতা না এনে শুধু বদলানো অংশের RSC payload আসে, আর সেটি একটিই roundtrip-এ। Remix (পরে React Router) এই “web platform-এর form-ই ভিত্তি” দর্শন আগেই জনপ্রিয় করেছিল; React 19 আর Next.js সেটিকে component-এর স্তরে নিয়ে গেছে।
তবে পুরোনো যুগের একটি শিক্ষা ভুলে গেলে বিপদ। PHP-র save.php একটি স্পষ্ট ফাইল ছিল, সবাই জানত এটি public. Server Action দেখতে একটি সাধারণ function, তাই অনেকে ভুলে যান এটিও একটি URL-এর মতো খোলা। নতুন বিমূর্ততা পুরোনো ঝুঁকি লুকিয়ে ফেলে, দূর করে না।
আরেকটি পার্থক্য: PHP-তে URL আপনি ঠিক করতেন, অন্য client-ও সেটি ডাকতে পারত। Action-এর ID build-এ বদলায়, তাই এটি অন্য client-এর জন্য স্থায়ী API নয়। এজন্যই মোবাইল অ্যাপের জন্য route handler আলাদা রাখতে হয়।
মূল উপলব্ধি: প্রযুক্তি চক্রে ফেরে, কিন্তু অভিজ্ঞতা নিয়ে। Server Action পুরোনো সরলতা আর নতুন মসৃণতা দুটোই দেয়, আর বিনিময়ে মনে রাখতে বলে যে function call-এর আড়ালে একটি খোলা HTTP দরজা আছে।
প্র ০২Action ID তো লম্বা, encrypted আর অনুমান-অযোগ্য। তাহলে “ID গোপন, তাই নিরাপদ” কেন ভুল যুক্তি?
এই যুক্তির নাম “security through obscurity”: লুকিয়ে রাখাকে পাহারা ভাবা। উনিশ শতকে Auguste Kerckhoffs সামরিক সংকেত নিয়ে লিখেছিলেন, একটি ব্যবস্থা এমন হওয়া উচিত যে শত্রু তার সব নকশা জানলেও key ছাড়া ভাঙতে পারবে না। পরে Claude Shannon একই কথা বলেন আরও ছোট করে: “শত্রু ব্যবস্থাটি জানে।” আধুনিক নিরাপত্তা এই নীতির ওপর দাঁড়িয়ে।
Action ID-র ক্ষেত্রে obscurity আরও দুর্বল, কারণ ID লুকানোই নেই। এই পর্বের ল্যাবে ID পাওয়া গেছে তিন জায়গায়: browser-এর chunk-এ createServerReference("4099…"), network tab-এর next-action header-এ, আর JavaScript ছাড়া form-এর লুকানো $ACTION_ID_… field-এ। যিনি আপনার সাইটে একবার বোতাম চাপতে পারেন, তিনি ID-ও পান। “অনুমান-অযোগ্য” কথাটির অর্থ শুধু এটুকু: যে action UI-তে কোথাও ব্যবহৃত হয়নি, তার ID বাইরের কেউ বানাতে পারবেন না। এজন্য Next.js অব্যবহৃত action-কে build থেকে বাদ দেয়। এটি ভালো, কিন্তু আপনার admin পাতার delete বোতাম তো ব্যবহৃত, তাই তার ID সেই পাতা দেখা যে কেউ পান।
Next.js-এর docs নিজেই বলে, এই ফিচারগুলো authentication না থাকলে ঝুঁকি কিছুটা কমায়, কিন্তু প্রতিটি action-কে সরাসরি POST-যোগ্য ধরে ভেতরে যাচাই করতে হবে। ঝুঁকির হিসাবও সহজ: ID ফাঁস হওয়ার সম্ভাবনা প্রায় নিশ্চিত, আর যাচাই না থাকলে ফল পুরো ডেটা মুছে যাওয়া। নিরাপত্তা তাই ID-র গোপনীয়তায় নয়, প্রতিটি ডাকে “আপনি কে, আপনার অধিকার আছে কি” প্রশ্নে।
মূল উপলব্ধি: যা browser পায়, তা ব্যবহারকারীও পান। ID একটি ঠিকানা, তালা নয়। তালা হলো action-এর ভেতরের authentication আর authorization.
প্র ০৩Origin header মেলানো থাকলে CSRF token আর লাগে না? Fetch Metadata, SameSite আর Origin মিলে ইতিহাসটা কী?
CSRF-এর মূল কথা: browser আপনার cookie প্রতিটি request-এ নিজে জুড়ে দেয়, এমনকি অন্য সাইট থেকে শুরু হওয়া request-এও। ২০০০-এর দশকে এর প্রধান সমাধান ছিল CSRF token: server form-এ একটি গোপন সংখ্যা বসায়, আর POST-এ সেটি মিলিয়ে দেখে। অন্য সাইট সেই সংখ্যা পড়তে পারে না, তাই জাল request ধরা পড়ে। Rails, Django, Laravel সবাই এটি ডিফল্টে দেয়।
তারপর browser নিজে সাহায্য শুরু করল। RFC 6454 (২০১১) Origin-এর ধারণা স্থির করে, আর browser POST-এ Origin header পাঠাতে থাকে, যা পাতার JavaScript বদলাতে পারে না। SameSite cookie এলো, আর ২০২০ সালে Chrome ডিফল্টে SameSite=Lax আচরণ চালু করে, যাতে অন্য সাইটের POST-এ cookie সাধারণত যায় না। পরে Fetch Metadata (Sec-Fetch-Site) আরও একটি সংকেত দেয়। পর্ব ৫০-এ এগুলো Express-এ বসিয়েছেন।
Next.js এই নতুন ভিত্তির ওপর বাজি ধরেছে: শুধু POST, আর Origin-এর host নিজের host-এর সাথে মেলা চাই। ল্যাবে evil.example থেকে আসা ডাক action পর্যন্ত পৌঁছায়নি। কিন্তু সীমাও আছে। Origin ছাড়া request docs অনুযায়ী সতর্কবার্তা দিয়ে পার হয়, আর ল্যাবেও তাই হয়েছে। Browser প্রায় সব POST-এ Origin পাঠায় বলে এটি সাধারণত স্ক্রিপ্টের পথ, যার হাতে শিকারের cookie নেই। আবার একই site-এর একটি subdomain-এ XSS থাকলে সেখান থেকে আসা request আসল origin-এরই হতে পারে; তখন কোনো CSRF পাহারাই কাজে আসে না।
তাহলে token লাগবে কি? সম্প্রদায়ে মতভেদ আছে। অনেকে Origin যাচাই, SameSite cookie আর POST-only মিলিয়ে যথেষ্ট মনে করেন, আর OWASP-ও Origin যাচাইকে একটি স্বীকৃত প্রতিরক্ষা বলে। খুব সংবেদনশীল কাজে (টাকা পাঠানো, password বদল) অনেক দল বাড়তি token বা আবার password চাওয়া রাখে।
মূল উপলব্ধি: CSRF-এর পাহারা স্তরে স্তরে। Framework একটি স্তর দেয়; cookie-র SameSite, XSS বন্ধ রাখা আর সংবেদনশীল কাজে আবার যাচাই, বাকি স্তরগুলো আপনার।
প্র ০৪ডিসেম্বর ২০২৫-এর React Server Components-এর গুরুতর ত্রুটি (CVE-2025-55182) থেকে Server Action ব্যবহারকারীরা কী শিখবেন?
প্রকাশিত advisory আর নিরাপত্তা প্রতিষ্ঠানের প্রতিবেদন অনুযায়ী, নভেম্বর ২০২৫-এর শেষে React দলকে একটি ত্রুটি জানানো হয়, আর ৩ ডিসেম্বর ২০২৫-এ সমাধান প্রকাশিত হয়। ত্রুটিটি ছিল Server Function-এর endpoint-এ আসা request-এর payload decode করার অংশে, আর প্রতিবেদনগুলো একে login ছাড়াই দূর থেকে কোড চালানোর (remote code execution) মতো গুরুতর বলে বর্ণনা করে। react-server-dom-webpack, -parcel আর -turbopack package-এর 19.0.0 থেকে 19.2.0-এর কয়েকটি সংস্করণ আক্রান্ত ছিল, ঠিক হয় 19.0.1, 19.1.2 আর 19.2.1-এ। App Router-এর মাধ্যমে Next.js 15 ও 16-ও আক্রান্ত ছিল, যার নিজস্ব CVE (CVE-2025-66478; পরে এটি CVE-2025-55182-এর duplicate হিসেবে বাতিল হয় বলে জানা যায়) আর patch সংস্করণ, যেমন 16.0.7. বিস্তারিত শর্ত জানতে React-এর নিজস্ব advisory পড়ুন, কারণ প্রতিবেদনগুলোর মধ্যে কোন অ্যাপ ঝুঁকিতে, তা নিয়ে কিছু পার্থক্য ছিল।
এখানে শিক্ষা তিনটি। প্রথম, এই পর্বের মূল কথাটি একটি বাস্তব ঘটনায় প্রমাণিত: Server Function-এর endpoint ইন্টারনেটের যে কারও কাছে খোলা, আর সেই দরজার decoder অচেনা মানুষের bytes পড়ে। আপনার নিজের action-এর ভেতরের যাচাই এই ধরনের framework-ত্রুটি ঠেকাতে পারে না, কারণ ত্রুটিটি আপনার কোড চলার আগেই।
দ্বিতীয়, dependency হালনাগাদ রাখা একটি নিরাপত্তার কাজ। পর্ব ৫০-এর npm audit, lock file আর নিয়মিত update এখানে সরাসরি প্রাসঙ্গিক; আর framework যে version-এ patch দিয়েছে, তার নিচে থাকা মানে জানা ঝুঁকি নিয়ে চলা।
তৃতীয়, স্তরে স্তরে প্রতিরক্ষা। Server-কে ন্যূনতম অধিকারে চালানো, secret-কে আলাদা রাখা, আর অস্বাভাবিক request-এর log দেখা, এগুলো কোনো একটি ত্রুটির ক্ষতি সীমিত রাখে।
মূল উপলব্ধি: নতুন বিমূর্ততা নতুন আক্রমণ-তলও আনে। “শুধু একটি function” লেখার সুবিধা নিলে সেই function-এর পেছনের protocol আর তার হালনাগাদের দায়ও নিতে হয়।
প্র ০৫Optimistic UI কি ব্যবহারকারীকে মিথ্যা বলা? কোথায় এটি ঠিক, আর কোথায় বিপজ্জনক?
মানুষের ধৈর্য নিয়ে Jakob Nielsen ১৯৯৩ সালে তিনটি সীমা লিখেছিলেন: প্রায় ০.১ সেকেন্ডে উত্তর এলে মনে হয় তাৎক্ষণিক, প্রায় ১ সেকেন্ডে চিন্তার ধারা ভাঙে না, আর প্রায় ১০ সেকেন্ডে মনোযোগ ধরে রাখা কঠিন। ঢাকার মোবাইল নেটওয়ার্কে একটি POST-এর roundtrip সহজেই কয়েকশো ms. Optimistic UI এই ফাঁকটুকু ঢাকে: ক্লিকের ০.১ সেকেন্ডের মধ্যে পর্দা বদলায়, সত্যিটা পরে আসে।
এটি মিথ্যা নয়, একটি পূর্বাভাস, যদি তিনটি শর্ত মানা হয়। (১) সফলতার সম্ভাবনা খুব বেশি: লাইক, প্রিয় তালিকা, ঝুড়ির সংখ্যা প্রায় সব সময় সফল হয়। (২) ব্যর্থতা হলে স্পষ্ট ফেরানো আর বার্তা: এই পর্বের run-এ বোতাম ২ থেকে ১-এ ফিরে “স্টক নেই” দেখিয়েছে। (৩) ভুল পূর্বাভাসের ক্ষতি কম: ক্ষণিকের জন্য ২ দেখানোয় কারও টাকা যায়নি।
যেখানে এই শর্ত ভাঙে, সেখানে optimistic বিপজ্জনক। “পেমেন্ট সফল” আগাম দেখিয়ে পরে ফেরানো মানে ক্রেতা হয়তো ততক্ষণে দোকান ছেড়েছেন, ভাবছেন অর্ডার হয়েছে। Stock-এর ক্ষেত্রে ঈদের আগে শেষ কয়েকটি পণ্যে অনেকে একসাথে চাপলে ব্যর্থতার হার বাড়ে, তখন বারবার rollback বিরক্তিকর। এমন জায়গায় সৎ “প্রক্রিয়াধীন” অবস্থা, যেমন useFormStatus-এর pending, ভালো।
আরেকটি সূক্ষ্মতা: optimistic মান কখনো server-এর ডেটা নয়। React-এর useOptimistic এটি নিশ্চিত করে, কারণ action শেষ হলেই অস্থায়ী মান হারায়, আর আসল state বা server-এর prop আবার দেখা যায়। হাতে লেখা optimistic কোডে অনেক সময় এই ফেরানোটাই ভুলে যাওয়া হয়, আর পর্দা ও database আলাদা গল্প বলে।
মূল উপলব্ধি: Optimistic UI গতির অনুভূতি দেয়, সত্য নয়। সম্ভাবনা বেশি আর ক্ষতি কম হলে আগাম দেখান; টাকা, অর্ডার আর আইনি অবস্থায় সত্যের অপেক্ষা করুন।
অনুশীলন
হিসাব, আউটপুট অনুমান, command আর নকশা সমালোচনা, চার রকম কাজ।
-
হিসাব: একজন ক্রেতা দ্রুত তিনটি “ঝুড়িতে দিন” বোতাম চাপলেন। প্রতিটি action-এ নেটওয়ার্কের roundtrip ১২০ ms আর server-এর কাজ ৮০ ms (ধরে নেওয়া)। Next.js একটি client থেকে action একটির পর একটি পাঠায়। শেষ উত্তর কত ms পরে আসবে? যদি তিনটি একসাথে যেত? আর প্রতিটি action
revalidatePathনা ডেকে পরে আলাদা একটি GET (আবার ১২০ + ৮০ ms) লাগত, তাহলে একটির পর একটি পথে মোট কত?প্রতিটি action: ১২০ + ৮০ = ২০০ ms. একটির পর একটি: ৩ × ২০০ = ৬০০ ms. একসাথে গেলে সবচেয়ে ধীরটির সমান, প্রায় ২০০ ms (server-এ race-এর দায় তখন transaction-এর)। আলাদা GET লাগলে প্রতিটি কাজ ২০০ + ২০০ = ৪০০ ms, মোট ৩ × ৪০০ = ১২০০ ms. একই উত্তরে নতুন পাতা আসায় অর্ধেক সময় বাঁচে, ৬০০ ms. আর
useOptimisticথাকলে ক্রেতা প্রথম ক্লিকের পরেই সংখ্যা বাড়তে দেখেন, এই ৬০০ ms তাঁর চোখে পড়ে না। -
আউটপুট অনুমান করুন: একটি action:
try { await db.save(x); redirect('/done') } catch (e) { return { error: 'সমস্যা' } }.db.saveসফল হলো। ব্যবহারকারী কোথায় যাবেন, আর action কী ফেরত দেবে? তারপর একই action-এdb.saveব্যর্থ হলে কী হবে?সফল হলেও ব্যবহারকারী একই পাতায় থাকবেন, আর action ফেরত দেবে
{ error: 'সমস্যা' }. কারণredirectএকটি বিশেষ error ছোড়ে, আর নিজেরcatchসেটি ধরে গিলে ফেলে। ব্যর্থ হলে একই{ error: 'সমস্যা' }, কোনো redirect নেই। সমাধান:try-এর ভেতরে শুধুdb.save, ব্যর্থ হলে সেখান থেকেই return;redirect('/done')try-এর পরে। Next.jsunstable_rethrowনামে একটি helper-ও দেয়, তবে সরল পথ হলো redirect বাইরে রাখা। -
Command: নিজের ল্যাবে
next buildআরnext start -H 127.0.0.1চালান। Network tab থেকে একটি action-এরnext-actionমান নিন, তারপর একটিnodeস্ক্রিপ্টেfetchদিয়ে cookie ছাড়া একই POST পাঠান (Content-Type: text/plain,Originনিজের host, body একটি JSON array). এরপরOrigin: http://evil.exampleদিয়ে আবার। দুই ক্ষেত্রে কী আশা করবেন, আর server log-এ কী খুঁজবেন?প্রথম ক্ষেত্রে action চলবে; ঠিকভাবে লেখা হলে এর ভেতরের login যাচাই একটি ফল ফেরত দেবে, যেমন
1:{"ok":false,"message":"আগে লগইন করুন"}, আর log-এ action-এর নিজের লাইন (user = none). দ্বিতীয় ক্ষেত্রে action চলবেই না: উত্তরে শুধুE{"digest":…}, log-এ “does not match `origin` header … Aborting the action” আরInvalid Server Actions request. শেষে server-টি নিজের process ID দিয়ে বন্ধ করুন। এই পরীক্ষা শুধু নিজের localhost ল্যাবে করবেন, অন্যের সাইটে নয়। -
নকশা সমালোচনা: AI এই কোড দিল: পাতায়
if (!session) redirect('/login'), আর একই পাতার form-এasync function update(fd) { 'use server'; await db.order.update({ where: { id: fd.get('id') }, data: { status: fd.get('status'), total: Number(fd.get('total')) } }); return await db.order.findUnique(…) }. অন্তত পাঁচটি সমস্যা বলুন, আর ঠিক করার পথ দিন।- Action-এ login নেই: পাতার redirect action রক্ষা করে না। ভেতরে
currentUser(). - Ownership নেই: যেকোনো
id-র অর্ডার বদলানো যায় (IDOR).where: { id, userId: user.id }. totalclient থেকে: দাম server হিসাব করবে, form থেকে নয়।status-ও শুধু অনুমোদিত পরিবর্তন, যেমন ক্রেতা শুধু “বাতিল”।- যাচাই নেই: Zod-এ
id,statusযাচাই, ভুলে field error ফেরত। - পুরো সারি return: শুধু UI-র যা লাগে ফেরত দিন।
- Revalidate নেই: বদলের পরে
revalidatePathবাupdateTag, নইলে পর্দা পুরোনো।
- Action-এ login নেই: পাতার redirect action রক্ষা করে না। ভেতরে
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJavaScript চালু থাকা অবস্থায় একটি Server Action-এর form জমা দিলে যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- Client action ID header-এ দিয়ে পাতার URL-এ POST পাঠায়
- Next.js Origin, body-র আকার আর ID যাচাই করে
- Action login, অনুমতি আর input যাচাই করে ডেটা বদলায়
- Action revalidatePath ডাকে আর Next.js একই request-এ পাতা আবার render করে
- একটি উত্তরে ফেরত মান আর নতুন RSC payload client-এ পৌঁছায়
Next.js-এর একটি Server Action browser থেকে কোন HTTP method-এ ডাকা হয়?
- GET
- POST
- PUT
- কাজ অনুযায়ী GET, POST বা DELETE
একটি admin পাতা শুরুতেই অ-admin ব্যবহারকারীকে redirect করে। সেই পাতার form-এ একটি delete action আছে, যার ভেতরে কোনো যাচাই নেই। কোন কথাটি ঠিক?
- পাতার redirect থাকায় action নিরাপদ
- Action ID গোপন, তাই কেউ ডাকতে পারবে না
- পাতা না খুলেই action সরাসরি ডাকা যায়, তাই ভেতরে যাচাই দরকার
- Origin যাচাই থাকায় অ-admin ডাকতে পারবেন না
একটি Server Action-এ লেখা: try { await save(); redirect('/done') } catch { return { error: 'ভুল' } }. save() সফল হলে কী হবে?
- Redirect হবে না, action ফেরত দেবে { error: 'ভুল' }
- ব্যবহারকারী /done-এ যাবেন
- error.js দেখাবে
- Action undefined ফেরত দেবে
Production build-এ একটি Server Action throw new Error('db password wrong') করল, আর error.tsx error.message দেখায়। ব্যবহারকারী কী দেখবেন?
- db password wrong
- একটি সাদা পাতা
- শুধু 500
- একটি সাধারণ বার্তা আর একটি digest, আসল বার্তা শুধু server log-এ
useOptimistic(count, (c, n) => c + n)-এ count = 1. ব্যবহারকারী চাপলেন, addShown(1) চলল, তারপর server ফেরত দিল “স্টক নেই”, count বদলায়নি। Action শেষ হলে পর্দায় কত?
- 2
- 1
- 0
- React error ছুড়বে
একটি মোবাইল অ্যাপ আর একটি payment gateway-র webhook, দুজনেরই একটি Next.js অ্যাপের সাথে কথা বলা দরকার। কোন পথ ঠিক?
- দুজনেই Server Action ডাকবে
- দুজনেই tRPC ব্যবহার করবে
- দুজনের জন্য route handler, আর নিজের UI-র form-এ Server Action
- Next.js দিয়ে সম্ভব নয়, আলাদা Express server লাগবে
একটি Server Component-এর inline Server Action বাইরের ভ্যারিয়েবল cost = 410 ব্যবহার করে। Next.js এই মান কীভাবে সামলায়?
- মানটি encrypt হয়ে browser-এ যায় আর action ডাকার সময় ফিরে আসে
- Server memory-তে মান রেখে দেয়, browser কিছু পায় না
- মানটি সাধারণ লেখা হিসেবে HTML-এ বসে
- Build ব্যর্থ হয়, closure ব্যবহার নিষেধ
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন, অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
"use server"মানে function-টি server-এ থাকে; browser পায় একটি ID, আর ডাক মানে পাতার URL-এ একটি POST.- Server Action-এর form JavaScript ছাড়াও জমা হয়;
onClick-এ ডাকা action হয় না। - প্রত্যাশিত ভুল ফেরত দিন (Zod-এর
fieldErrorsআরvalues), অপ্রত্যাশিত ভুল ছুড়ুন; production-এ client পায় শুধু digest. - বদলের পরে
revalidatePath/updateTag, তারপর try/catch-এর বাইরেredirect; একটি roundtrip-এই নতুন পর্দা। - প্রতিটি action একটি public endpoint: ভেতরে login, role, ownership, input যাচাই আর rate limit; framework দেয় শুধু Origin, body সীমা আর closure encryption.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৫ · SSR, SSG, ISR আর partial prerendering — কখন কোনটিপরবর্তী পর্বপাতা কখন তৈরি হবে: build-এ, প্রতি অনুরোধে, নাকি মাঝেমধ্যে নতুন করে।
- ফুল-স্ট্যাক ওয়েব ফ্রেমওয়ার্ক · কোর্স হোম৯৩টি পর্বপুরো কোর্সের রোডম্যাপ; পরের পর্ব ৫৫, SSR, SSG, ISR আর partial prerendering.
- পর্ব ১২ · Form — controlled input, submit আর ব্যবহারকারীর ইনপুটভিত্তিuseActionState আর useFormStatus-এর শুরু।
- পর্ব ৪৮ · Permission — role, ownership আর দুই দিকের যাচাইভিত্তিIDOR আর ownership-এর পূর্ণ ল্যাব।
- CSRF আর cookieগভীরে যানOrigin, SameSite আর token-এর তত্ত্ব।
- Broken access controlগভীরে যানকেন অনুমতির ভুল সবচেয়ে সাধারণ ঝুঁকি।
- Rate limitingগভীরে যানFixed window, sliding window আর token bucket.