সরাসরি পর্বে যান
পর্ব ৩৫ · ৫৯-এর মধ্যে · মডিউল ৫

fetch, HTTP আর JSON API — সার্ভারের সাথে কথা বলা

JavaScript fetch — HTTP Requests & Responses, URLs, Methods, Status Codes, response.ok, JSON APIs, Errors, Timeouts and CORS

Taught in Bangla, this lesson explains talking to servers from the browser: request and response structure, building URLs safely with URLSearchParams, HTTP methods, status codes and headers, the two-step fetch promise, checking res.ok with a reusable getJSON helper, posting JSON and FormData, network errors versus HTTP errors, timeouts, cookies, CORS and loading states.

এই পর্বে যা শিখবেন

  • HTTP-র request আর response-এর গঠন; URL-এর অংশ আর URL/URLSearchParams দিয়ে নিরাপদ ঠিকানা
  • GET/POST/PUT/PATCH/DELETE, idempotency, status code-এর পরিবার আর দরকারি header; REST আর JSON API
  • fetch-এর ভেতরের দুই ধাপ, body একবার পড়ার নিয়ম, res.ok আর পুনর্ব্যবহারযোগ্য getJSON helper
  • POST দিয়ে JSON পাঠানো, 201 আর 422 সামলানো, FormData আর ফাইল আপলোড
  • Network error বনাম HTTP error, timeout, cookie, CORS, cache, UI-তে loading/error অবস্থা — আর AI-এর লেখা fetch কোড যাচাই

১ · পাতা রিলোড ছাড়াই সার্ভারের সাথে কথা — request আর response

একটি অনলাইন বাজারের ওয়েবসাইট খুলুন। চালের দাম হঠাৎ বদলে গেল, অথচ আপনি পাতা রিলোড করেননি। “অর্ডার করুন” চাপলেন, বোতামের পাশে ছোট্ট একটি ঘূর্ণি ঘুরল, তারপর লেখা এল “অর্ডার নম্বর A-101”. পুরো পাতা একবারও নতুন করে লোড হয়নি। পেছনে যা ঘটল তা হলো: পাতার JavaScript চুপচাপ দূরের একটি সার্ভারকে বার্তা পাঠিয়েছে, উত্তর পেয়েছে, আর সেই উত্তর দিয়ে পাতার একটি অংশ বদলে দিয়েছে। এই বার্তা আদান-প্রদানের ভাষার নাম HTTP — HyperText Transfer Protocol.

HTTP-র কথোপকথন সবসময় একই ছাঁচে হয়। একপক্ষ — client, সাধারণত ব্রাউজার বা মোবাইল অ্যাপ — একটি request পাঠায়। অন্যপক্ষ — server — একটি response ফেরত দেয়। সার্ভার নিজে থেকে কখনো কথা শুরু করে না; প্রতিটি উত্তর কোনো একটি অনুরোধের জবাব (সার্ভার থেকে নিজে বার্তা পাঠানোর ব্যবস্থা পর্ব ৫১-এ)। দুই বার্তারই নির্দিষ্ট গঠন আছে:

একটি HTTP কথোপকথন

request: method + URL + header + (body) → response: status + header + body

  1. Client (ব্রাউজার)fetch(url, options)পাতার JavaScript
  2. RequestGET /api/productsmethod, URL, header, body
  3. ServerNode, Java, Python…ডেটাবেস দেখে উত্তর বানায়
  4. Response200 OK + JSONstatus, header, body
ছবি ৩৫.১ — প্রতিটি HTTP আদান-প্রদান একটি request আর তার একটি response. মাঝখানে যায় শুধু লেখা বা byte; তাই ডেটা সাধারণত JSON হয়ে যাতায়াত করে (পর্ব ১৯)।

পুরান ঢাকার একটি হোটেলে খেতে বসেছেন। আপনি (client) বেয়ারাকে একটি স্লিপ দিলেন: কী চাই (method — “দিন”), কোন টেবিলের জন্য, কোন পদ (URL), সাথে বিশেষ নির্দেশ — “ঝাল কম, প্লেট আলাদা” (header), আর বড় অর্ডার হলে আলাদা কাগজে পুরো তালিকা (body). রান্নাঘর (server) ফেরত পাঠাল প্লেট — সাথে একটি ছোট চিরকুট: “পরিবেশিত” বা “শেষ হয়ে গেছে” (status), “গরম, সাবধানে” (header), আর আসল খাবার (body). খেয়াল করুন: “শেষ হয়ে গেছে” চিরকুট এলেও বেয়ারা তো ফিরে এসেছেন — যোগাযোগ সফল, শুধু খবরটা খারাপ। fetch-এর সবচেয়ে বড় চমকটি ঠিক এখানেই লুকানো (অংশ ৪)।

ব্রাউজারে JavaScript দিয়ে এই কাজের আধুনিক পথ হলো fetch() — একটি Web API, ভাষার অংশ নয়, কিন্তু সব আধুনিক ব্রাউজারে আর Node.js 18 থেকে Node-এও আছে। fetch তৈরি হয়েছে Promise-এর ওপর, তাই পর্ব ৩৪-এর async/await দিয়েই এর সব কাজ লেখা যায়। আর যেসব সার্ভার প্রোগ্রামকে ডেটা দেওয়ার জন্যই বানানো, তাদের বলে API — Application Programming Interface: মানুষের জন্য HTML পাতা নয়, প্রোগ্রামের জন্য JSON.

মূল ধারণা

fetch শুধু এটুকু কথা দেয়: “সার্ভারের কাছ থেকে একটি উত্তর এনে দেব।” উত্তরটি ভালো খবর (200) না খারাপ খবর (404, 500) — সেটি বিচার করা আপনার কাজ। Promise reject হয় কেবল তখন, যখন উত্তরই আসেনি।

১৯৯০–৯১ সালে CERN-এ Tim Berners-Lee প্রথম ওয়েব সার্ভার, ব্রাউজার আর HTTP বানান। প্রথম HTTP (পরে নাম হয় HTTP/0.9) এতই সরল ছিল যে এর একটিমাত্র method ছিল — GET, আর কোনো header বা status code ছিল না। আজকের HTTP/2 আর HTTP/3 পর্যন্ত যাত্রার গল্প ভাবনার প্রশ্ন ১-এ।

২ · URL — ঠিকানার অংশ, আর query string নিরাপদে বানানো

প্রতিটি request যায় একটি URL-এ। একটি URL ভাঙলে কয়েকটি অংশ পাওয়া যায়, আর প্রতিটির আলাদা কাজ:

https://shop.example.com:8080/api/products?category=rice&page=2#top
অংশউদাহরণকাজ
protocol (scheme)https:কোন নিয়মে কথা হবে; https মানে এনক্রিপ্ট করা HTTP
hostshop.example.comকোন যন্ত্র — DNS নামটিকে IP ঠিকানায় বদলায়
port8080যন্ত্রের কোন দরজা; না লিখলে https-এ 443, http-এ 80
pathname/api/productsসার্ভারের কোন জিনিস (resource) চাই
search (query string)?category=rice&page=2বাড়তি শর্ত — key=value জোড়া, & দিয়ে আলাদা
hash (fragment)#topশুধু ব্রাউজারের জন্য — সার্ভারে যায়ই না

এখন একটি সাধারণ ভুল দেখুন। সার্চ বক্সে কেউ লিখলেন চা & দই, আর কোড হাতে জোড়া দিল: "/api/products?q=" + text + "&page=2". ফল দাঁড়াল ?q=চা & দই&page=2. সার্ভারের চোখে & মানে “নতুন জোড়া শুরু”, তাই সে পড়ে q = "চা " আর " দই" নামে একটি খালি key — খোঁজার শব্দ অর্ধেক হারিয়ে গেল। কেউ ইচ্ছা করে x&admin=true লিখলে বাড়তি শর্ত ঢুকে যেত। URL-এ বিশেষ অক্ষর (&, =, ?, #, ফাঁকা) আর বাংলার মতো non-ASCII লেখা পাঠাতে হয় percent-encoding করে — প্রতিটি byte %XX আকারে। এই কাজের সঠিক যন্ত্র হলো built-in URL আর URLSearchParams:

1const url = new URL("/api/products", "https://shop.example.com");2url.searchParams.set("category", "rice");3url.searchParams.set("q", "চা & দই");4url.searchParams.set("page", 2);5console.log(url.searchParams.get("q"), url.href);
url-এর অবস্থা
pathname
search
q (পড়লে)
  1. Base থেকে URL বানানো। new URL(path, base) আপেক্ষিক পথকে পূর্ণ ঠিকানায় মেলায় — ব্রাউজারে base হিসেবে সাধারণত location.origin দেওয়া হয়। ভুল ঠিকানা দিলে URL সাথে সাথে TypeError ছোড়ে; ভুল ধরা পড়ে request পাঠানোর আগেই।
  2. searchParams.set. প্রতিটি URL-এর সাথে একটি জীবন্ত URLSearchParams থাকে। এতে key-value বসালে url.search নিজে থেকে বদলায় — ? আর & কোথায় বসবে, সে চিন্তা আপনার নয়।
  3. বাংলা আর & — নিরাপদে encode. “চা” দুটি অক্ষর, UTF-8-এ প্রতিটি ৩ byte, তাই হলো %E0%A6%9A%E0%A6%BE. ফাঁকা হলো +, আর & হলো %26 — এখন সেটি আর জোড়া ভাঙবে না। (encodeURIComponent ফাঁকাকে লেখে %20; সার্ভার দুটোই বোঝে।)
  4. সংখ্যাও string. 2 দিলেও query-তে সব মান string হয়ে যায় — সার্ভারে পৌঁছায় "2". সার্ভারকে আবার সংখ্যায় বদলে যাচাই করতে হয় (পর্ব ৫০)। set একই key আগের মান মুছে বসায়; একই key-তে একাধিক মান চাইলে append.
  5. পড়লে আবার আসল লেখা। get("q") decode করে আসল চা & দই ফেরত দেয়, আর url.href হলো পাঠানোর জন্য প্রস্তুত পূর্ণ ঠিকানা — সরাসরি fetch(url)-এ দেওয়া যায়। সার্ভারেও একই URL class দিয়ে query পড়া যায় — নিচের Node উদাহরণে দেখবেন।

ছোট কাজে আরও সংক্ষেপে লেখা যায়: new URLSearchParams({ category: "rice", page: 2 }) একটি object থেকেই query বানায়, আর template literal-এ বসালে নিজে থেকে string হয় — `/api/products?${params}`. Path-এর ভেতরে ব্যবহারকারীর লেখা বসাতে হলে (/api/users/${name}) encodeURIComponent(name) দিন; নইলে নামের / পথটিকেই বদলে দেবে।

৩ · Method, status code আর header — HTTP-র শব্দভান্ডার

Method — কী করতে চাই

Request-এর প্রথম শব্দটি HTTP method — সার্ভারকে জানায় আপনার উদ্দেশ্য কী। ওয়েব API-তে পাঁচটি method-ই বেশি দেখবেন:

MethodকাজউদাহরণBody?Idempotent?
GETপড়া, কিছু বদলায় নাGET /api/products/7নাহ্যাঁ (আর “safe”)
POSTনতুন কিছু তৈরি বা কোনো কাজ চালানোPOST /api/ordersহ্যাঁনা
PUTপুরোটা বদলে বসানোPUT /api/cart/items/7হ্যাঁহ্যাঁ
PATCHকিছু অংশ বদলানোPATCH /api/users/12হ্যাঁনিশ্চিত নয়
DELETEমুছে ফেলাDELETE /api/cart/items/7সাধারণত নাহ্যাঁ

শেষ কলামটি গুরুত্বপূর্ণ। কোনো অনুরোধ idempotent মানে একবার পাঠানো আর পরপর দশবার পাঠানোর ফল সার্ভারের অবস্থায় একই। “কার্টে item 7-এর পরিমাণ ৩ করুন” (PUT) দশবার পাঠালেও পরিমাণ ৩-ই থাকে। কিন্তু “নতুন অর্ডার দিন” (POST) দশবার গেলে দশটি অর্ডার! নেটওয়ার্ক মাঝপথে কেটে গেলে আপনি জানেন না অনুরোধ পৌঁছেছিল কি না — idempotent অনুরোধ তখন নিশ্চিন্তে আবার পাঠানো যায়, POST নয়। এজন্য পেমেন্ট API-গুলো (যেমন অনেক payment gateway) POST-এর সাথে একটি Idempotency-Key header চায়, যাতে একই key দুবার এলে সার্ভার দ্বিতীয়টি চিনে বাদ দিতে পারে। Retry নিয়ে আরও কথা পর্ব ৩৬-এ।

Status code — উত্তরের সারকথা এক সংখ্যায়

Response-এর প্রথম লাইনে থাকে তিন অঙ্কের status code. প্রথম অঙ্ক দেখেই পরিবার চেনা যায়: 2xx সফল, 3xx অন্য জায়গায় দেখুন, 4xx client-এর ভুল (অনুরোধে সমস্যা), 5xx server-এর ভুল। যেগুলো সবচেয়ে বেশি দেখবেন:

Codeনামকখন
200OKসফল, body-তে ফল
201CreatedPOST দিয়ে নতুন জিনিস তৈরি হয়েছে; প্রায়ই Location header-এ তার ঠিকানা
204No Contentসফল, কিন্তু body নেই (যেমন DELETE-এর পর) — res.json() ডাকলে SyntaxError
301 / 304Moved Permanently / Not Modifiedস্থায়ীভাবে সরে গেছে (fetch নিজে পিছু নেয়) / আপনার cache-এর কপিই এখনো ঠিক আছে
400Bad Requestঅনুরোধের গঠনই ভুল — ভাঙা JSON, অনুপস্থিত field
401 / 403Unauthorized / Forbidden401: আপনি কে জানি না (লগইন লাগবে); 403: চিনি, কিন্তু অনুমতি নেই
404Not Foundএই ঠিকানায় কিছু নেই
409Conflictবর্তমান অবস্থার সাথে সংঘাত — যেমন ইমেইলটি আগেই নিবন্ধিত
422Unprocessable Contentগঠন ঠিক, কিন্তু মান অগ্রহণযোগ্য — পরিমাণ ০, ফোন নম্বর ভুল
429Too Many Requestsখুব দ্রুত বেশি অনুরোধ; Retry-After header-এ অপেক্ষার সময়
500 / 503Internal Server Error / Service Unavailableসার্ভারের কোডে ভাঙন / সার্ভার ব্যস্ত বা রক্ষণাবেক্ষণে — একটু পরে আবার চেষ্টা

Header — বার্তার খামের ওপর লেখা তথ্য

Header হলো নাম: মান আকারের লাইন — body-র বাইরে বাড়তি তথ্য। নাম case মানে না (content-type আর Content-Type একই)। সবচেয়ে দরকারি কয়েকটি: Content-Type জানায় body কোন ফরম্যাটে (application/json, text/html, multipart/form-data); Accept জানায় client কোন ফরম্যাটে উত্তর চায়; Authorization-এ থাকে পরিচয়ের প্রমাণ, যেমন Bearer <token>; আর Cache-Control, ETag ঠিক করে উত্তর কতক্ষণ জমিয়ে রাখা যাবে (অংশ ৮)। JavaScript-এ res.headers.get("content-type") দিয়ে পড়া যায়।

REST আর JSON API

বেশিরভাগ ওয়েব API একটি সহজ রীতি মেনে বানানো হয়, যাকে বলে REST: সার্ভারের সবকিছু হলো resource (পণ্য, অর্ডার, ব্যবহারকারী), প্রতিটির একটি ঠিকানা আছে, আর কী করবেন তা বলে method. পণ্যের তালিকা GET /api/products, একটি পণ্য GET /api/products/7, নতুন অর্ডার POST /api/orders, একটি অর্ডার বাতিল DELETE /api/orders/A-101. প্রতিটি “method + ঠিকানা” জোড়াকে বলে একটি endpoint. URL-এ ক্রিয়াপদ (/getProducts, /deleteOrder?id=…) না রেখে নামপদ রাখা আর কাজ method-কে দেওয়া — এটুকু মানলেই API অনেক বেশি অনুমানযোগ্য হয়। এমন একটি API নিজে বানাবেন পর্ব ৫০-এ।

৪ · fetch() — একটি অনুরোধের জীবন, আর দুই ধাপের Promise

সবচেয়ে সরল ব্যবহার এক লাইনের: const res = await fetch("/api/products?category=rice&page=2");. দ্বিতীয় argument-এ একটি options object দেওয়া যায় — method, headers, body, signal, credentials, cache ইত্যাদি। কিন্তু ভেতরে আসলে কী ঘটে, আর কেন একটি নয়, দুটি await লাগে? ধাপে ধাপে দেখুন:

  1. Request তৈরিfetch(url)Promise #১ — pending
  2. পাঠানোGET … HTTP/1.1DNS, সংযোগ, TLS
  3. Header এলres.statusPromise #১ পূর্ণ
  4. Body পড়াres.json()Promise #২
  5. JavaScript objectdata.itemsব্যবহারের জন্য প্রস্তুত
আপনার কোডfetch সাথে সাথে ফেরে — উত্তর এখনো আসেনি
const p = fetch("/api/products?category=rice&page=2",
  { headers: { Accept: "application/json" } });
console.log(p);        // Promise { <pending> }
const res = await p;   // এখানে অপেক্ষা — main thread কিন্তু খালি
নেটওয়ার্কে যা গেলব্রাউজার নিজে কিছু header যোগ করে
GET /api/products?category=rice&page=2 HTTP/1.1
Host: shop.example.com
Accept: application/json
User-Agent: Mozilla/5.0 …         ← ব্রাউজার যোগ করেছে
Cookie: session=…                 ← একই origin হলে
প্রথম Promise পূর্ণbody এখনো পথে
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60

// JavaScript-এ:
res.status    // 200
res.ok        // true  (200–299 হলে true)
res.bodyUsed  // false
// 404 হলেও এই ধাপে Promise পূর্ণ হয় — শুধু ok হতো false
দ্বিতীয় Promisebody আসে টুকরো টুকরো (stream)
const data = await res.json();   // বা res.text(), res.blob()
// টুকরো ১: {"category":"rice","items":[{"id":7,"na
// টুকরো ২: me":"মিনিকেট চাল ৫ কেজি","price":420}, …
// সব টুকরো জমিয়ে → UTF-8 decode → JSON.parse
ফলbody এখন “খরচ” হয়ে গেছে
data.items[0].name   // "মিনিকেট চাল ৫ কেজি"
res.bodyUsed         // true
await res.text();     // TypeError — body একবারই পড়া যায়
  1. Request তৈরি, Promise ফেরত। fetch সাথে সাথে একটি pending Promise ফেরত দেয় আর আসল কাজ ব্রাউজারের নেটওয়ার্ক অংশকে দিয়ে দেয়। await-এর সময় আপনার function থামে, কিন্তু পাতা জমে যায় না — event loop অন্য কাজ চালিয়ে যায় (পর্ব ৩১)।
  2. পাঠানো। ব্রাউজার host-এর IP খোঁজে (DNS), সংযোগ খোলে, https হলে এনক্রিপশন সেরে request পাঠায়। কিছু header ব্রাউজার নিজে যোগ করে, যেগুলো JavaScript বদলাতে পারে না (Host, Cookie-এর মতো; Chrome-এ User-Agent-ও)। এই ধাপে সংযোগ না হলে — ইন্টারনেট নেই, DNS ব্যর্থ — Promise reject হবে।
  3. Header এল — প্রথম Promise পূর্ণ। Status লাইন আর header পৌঁছামাত্র Promise একটি Response object দিয়ে পূর্ণ হয়। এখানেই সেই চমক: 404 বা 500 এলেও Promise পূর্ণ হয়, কারণ উত্তর তো এসেছে। পার্থক্য শুধু res.ok (status ২০০–২৯৯ হলে true) আর res.status-এ। Redirect (301, 302) fetch নিজে অনুসরণ করে, তাই সাধারণত শেষ গন্তব্যের উত্তরই দেখেন (res.redirected, res.url).
  4. Body পড়া — দ্বিতীয় Promise. Body বড় হতে পারে আর আসে ধীরে, টুকরো টুকরো (ReadableStream). তাই body পড়ার method-গুলোও Promise ফেরত দেয়: res.json() (পড়ে + JSON.parse), res.text() (লেখা হিসেবে), res.blob() (ছবি, PDF-এর মতো binary), res.arrayBuffer(), res.formData(). Body JSON না হলে res.json() SyntaxError দিয়ে reject হয়।
  5. ফল — আর body একবারই। Stream একবার পড়া হয়ে গেলে শেষ; পানির পাইপের পানি দুবার খাওয়া যায় না। দ্বিতীয়বার res.text() ডাকলে TypeError. দুবার লাগলে আগে থেকে res.clone() নিন, বা একবার text() পড়ে নিজে JSON.parse করুন। এজন্যই “error হলে res.text(), না হলে res.json()” — এমন সিদ্ধান্ত নিতে হয় body ছোঁয়ার আগে.

AI আর পুরোনো tutorial-এ সবচেয়ে বেশি দেখা কোড: const data = await (await fetch(url)).json(); — এক লাইনে সুন্দর, কিন্তু 404-এ কী হবে? সার্ভার হয়তো JSON-এ {"error":"not found"} পাঠাল — কোড সেটিকেই পণ্য ভেবে data.price পড়বে, পাবে undefined, আর পাতায় দেখাবে “৳ undefined”. বা সার্ভার HTML error পাতা পাঠাল — তখন SyntaxError: Unexpected token '<', যা আসল সমস্যা (সার্ভার 502) লুকিয়ে ফেলে। নিয়ম: body পড়ার আগে সবসময় res.ok দেখুন।

৫ · res.ok যাচাই — getJSON helper আর HttpError

প্রতিটি fetch-এর পাশে একই যাচাই লিখলে কোড পুনরাবৃত্তিতে ভরে যায়, আর কোথাও না কোথাও ভুলে যাবেনই। পেশাদার কোডে তাই একটি ছোট helper থাকে — getJSON(url) বা api(path, options) — যা তিনটি সম্ভাব্য ফলকে তিনটি স্পষ্ট পথে পাঠায়। সিদ্ধান্তের গাছটি দেখুন:

  1. await fetchres = ?তিন রকম ফল সম্ভব
  2. Rejectcatch → TypeErrorউত্তরই আসেনি
  3. res.ok?200–299উত্তর এসেছে, বিচার করুন
  4. !okthrow HttpErrorstatus আর body সাথে
  5. okreturn res.json()204 হলে body নেই
Network errorব্রাউজারভেদে বার্তা আলাদা
TypeError: Failed to fetch                          ← Edge/Chrome
TypeError: NetworkError when attempting to fetch resource.  ← Firefox
TypeError: fetch failed   (cause: ECONNREFUSED …)   ← Node.js
→ “ইন্টারনেট সংযোগ দেখে আবার চেষ্টা করুন”
উত্তর এসেছেPromise পূর্ণ — ভালো বা খারাপ খবর
if (!res.ok) { … }        // ok = status 200–299
// এখনো body ছোঁয়া হয়নি — কোন method-এ পড়ব তা পরে ঠিক হবে
HTTP errorনিজের error class (পর্ব ২৯)
const body = isJSON ? await res.json() : await res.text();
throw new HttpError(res.status, body);
// 401 → লগইন পাতায় · 404 → “পণ্যটি নেই” · 422 → ফর্মে লাল দাগ
// 429/503 → একটু পরে আবার চেষ্টা · 500 → “আমাদের দিকে সমস্যা”
সফলএখনো একটি ফাঁদ বাকি
if (res.status === 204) return null;   // body নেই
return await res.json();   // JSON না হলে SyntaxError — সেটাও ধরা পড়বে
  1. অপেক্ষার পর তিন রাস্তা। await fetch(url) শেষ হলে ঠিক তিনটির একটি ঘটে: Promise reject হয়েছে; উত্তর এসেছে আর সফল; উত্তর এসেছে কিন্তু ব্যর্থ। ভালো helper তিনটিকে আলাদা করে চেনায়, যাতে UI প্রতিটির জন্য আলাদা বার্তা দেখাতে পারে।
  2. Reject — উত্তরই আসেনি। ইন্টারনেট নেই, DNS ব্যর্থ, সার্ভার বন্ধ, CORS-এ আটকে গেছে, বা আপনি নিজে abort করেছেন। ফল network error: একটি TypeError, যার বার্তা ব্রাউজারভেদে আলাদা — তাই বার্তার লেখা মিলিয়ে সিদ্ধান্ত নেবেন না। নিরাপত্তার কারণে ব্রাউজার কারণটিও জানায় না; বিস্তারিত দেখতে হয় DevTools-এর Console আর Network-এ।
  3. উত্তর এসেছে — এখন বিচার। res.ok একটি সুবিধার property: status ২০০ থেকে ২৯৯ হলে true. এই পরীক্ষাটি body পড়ার আগে, কারণ ফলের ওপর নির্ভর করে body কীভাবে পড়বেন।
  4. ব্যর্থ — নিজের error ছুড়ুন। পর্ব ২৯-এর custom error-এর কৌশলে HttpError বানান, যার ভেতরে status আর সার্ভারের পাঠানো body থাকে। Body JSON কি না Content-Type দেখে ঠিক করুন — error-এর সময় অনেক সার্ভার HTML পাঠায়। ডাকার কোড তখন e instanceof HttpError && e.status === 404 দেখে সঠিক বার্তা দেখাতে পারে।
  5. সফল — তবু সাবধান। 204 No Content-এ body নেই, res.json() ডাকলে SyntaxError; তাই আলাদা করে ধরুন। আর “সফল” উত্তরের body-ও যাচাই করা দরকার — পর্ব ১৯-এর নিয়মে আকার আর টাইপ পরখ করুন, কারণ API বদলাতে পারে।

এবার পুরো ধারণাটি চালিয়ে দেখুন। এই পাতার কোড-বক্সে ইন্টারনেট নেই, তাই আমরা একটি নকল fakeFetch বানিয়েছি, যা সত্যিকারের Response object ফেরত দেয় — ব্রাউজারের আসল fetch ঠিক এই class-এর object-ই দেয়। একটি পণ্য পাওয়া যায়, একটি 404, আর একটি 502 যার body HTML:

const products = { 7: { name: "মিনিকেট চাল ৫ কেজি", price: 420 } };
const reply = (body, status, type = "application/json") =>
  new Response(body, { status, headers: { "Content-Type": type } });
async function fakeFetch(url) {                 // নেটওয়ার্ক ছাড়া "সার্ভার"
  const id = url.split("/").at(-1);
  if (id === "boom") return reply("<h1>502 Bad Gateway</h1>", 502, "text/html");
  return products[id] ? reply(JSON.stringify(products[id]), 200) : reply('{"error":"পণ্য পাওয়া যায়নি"}', 404);
}
class HttpError extends Error {
  name = "HttpError";
  constructor(status, body) { super(`HTTP ${status}`); this.status = status; this.body = body; }
}
async function getJSON(url) {
  const res = await fakeFetch(url);              // 404 হলেও এখানে reject হয় না
  const body = res.headers.get("Content-Type")?.includes("json") ? await res.json() : await res.text();
  if (!res.ok) throw new HttpError(res.status, body);
  return body;
}
(async () => {
  for (const id of ["7", "99", "boom"]) {
    try { const p = await getJSON(`/api/products/${id}`); console.log("পেলাম:", p.name, p.price); }
    catch (e) { console.log(e.name, e.status, e.body.error ?? e.body); }
  }
  const res = await fakeFetch("/api/products/7");
  console.log(res.ok, res.status, res.bodyUsed, await res.json(), res.bodyUsed);
  await res.text().catch((e) => console.log(`${e.name}: ${e.message}`));
})();

প্রথম লাইন পেলাম: মিনিকেট চাল ৫ কেজি 420. তারপর HttpError 404 পণ্য পাওয়া যায়নি — fakeFetch reject হয়নি, getJSON নিজে res.ok দেখে error ছুড়েছে, আর সার্ভারের JSON বার্তাটি e.body-তে পৌঁছে গেছে। HttpError 502 <h1>502 Bad Gateway</h1> লাইনে দেখুন: Content-Type HTML বলে body পড়া হয়েছে text() দিয়ে — json() ডাকলে আসত বিভ্রান্তিকর SyntaxError. শেষ দুই লাইন body একবার পড়ার নিয়ম দেখায়: true 200 false { name: 'মিনিকেট চাল ৫ কেজি', price: 420 } true — পড়ার আগে bodyUsed ছিল false, পরে true. তারপর দ্বিতীয়বার পড়তে গিয়ে Edge/Chrome বলে TypeError: Failed to execute 'text' on 'Response': body stream already read; একই ভুলে Node.js বলে TypeError: Body is unusable: Body has already been read. (কোড চালানোর পরিবেশের ক্লাসিক script-এ top-level await চলে না বলে কাজটি একটি async arrow function-এ মোড়ানো।)

৬ · ডেটা পাঠানো — POST JSON, 201 বনাম 422, আর FormData

GET দিয়ে ডেটা আনা হলো, এবার পাঠানোর পালা। অর্ডার দিতে তিনটি জিনিস লাগে: method: "POST", body কোন ফরম্যাটে তা জানাতে Content-Type header, আর body নিজে — string হিসেবে। JavaScript object সরাসরি body-তে দিলে fetch তাকে "[object Object]" লেখায় বদলে পাঠায়; তাই JSON.stringify অবশ্যই। Request আর উত্তর দুটোকেই খুলে দেখুন:

ধাপ ১ · Request linemethod + path + সংস্করণ
POST /api/orders HTTP/1.1
Host: shop.example.com
ধাপ ২ · Headerbody-র পরিচয় আর আপনার পরিচয়
POST /api/orders HTTP/1.1
Host: shop.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 24          ← ব্রাউজার নিজে গোনে
ধাপ ৩ · ফাঁকা লাইন, তারপর bodyJSON.stringify-এর ফল
POST /api/orders HTTP/1.1
Host: shop.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 24

{"productId":7,"qty":2}
ধাপ ৪ · সফল উত্তরনতুন জিনিস তৈরি হয়েছে
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/orders/A-101

{"id":"A-101","productId":7,"qty":2}
ধাপ ৫ · যাচাইয়ে ব্যর্থqty: 0 পাঠালে
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{"errors":{"qty":"পরিমাণ অন্তত ১ হতে হবে"}}
// res.ok === false → HttpError → ফর্মের qty ঘরের নিচে লাল লেখা
  1. Request line. প্রথম লাইনে method (POST), সার্ভারের ভেতরের পথ আর HTTP সংস্করণ। fetch(url, { method: "POST" }) থেকেই এটি তৈরি। method না দিলে ডিফল্ট GET — আর GET-এ body দিলে fetch TypeError দেয়।
  2. Header. Content-Type: application/json সার্ভারকে বলে “body-টি JSON হিসেবে পড়ুন।” এটি না দিলে string body-র জন্য fetch পাঠায় text/plain;charset=UTF-8, আর অনেক সার্ভার ফ্রেমওয়ার্ক তখন body-কে খালি ধরে নেয় — “সার্ভার আমার ডেটা পাচ্ছে না” ধরনের বাগের সবচেয়ে সাধারণ কারণ এটিই। লগইন-করা API-তে এখানে Authorization header-ও থাকে।
  3. ফাঁকা লাইন আর body. Header শেষ হয় একটি ফাঁকা লাইনে, তারপর body — JSON.stringify({ productId: 7, qty: 2 })-এর ফল। দাম পাঠানো হয়নি, খেয়াল করুন: দাম ঠিক করবে সার্ভার নিজের ডেটাবেস দেখে। Client-এর পাঠানো দাম বিশ্বাস করা মানে যে কেউ DevTools থেকে ১ টাকায় চাল কিনতে পারবে।
  4. 201 Created. সার্ভার অর্ডার তৈরি করে 201 ফেরত দিল, Location header-এ নতুন অর্ডারের ঠিকানা আর body-তে পুরো অর্ডার (সার্ভারের দেওয়া id-সহ)। res.ok এখানে true, কারণ ২০১ তো ২০০–২৯৯-এর মধ্যে।
  5. 422 — যাচাইয়ে ব্যর্থ। গঠন ঠিক থাকলেও মান অগ্রহণযোগ্য, তাই 422 আর body-তে কোন field-এ কী সমস্যা। ভালো API এভাবে field-ধরে error পাঠায়, যাতে ফর্ম ঠিক ঘরের নিচে বার্তা দেখাতে পারে। Front-end-এর নিজস্ব যাচাই (পর্ব ৩৯) ব্যবহারকারীর সুবিধার জন্য; আসল পাহারাদার সবসময় সার্ভার।

এখন সত্যিকারের নেটওয়ার্ক। নিচের বক্সটি Node.js 20-এ চলে, যেখানে fetch global হিসেবে আছে। একই প্রোগ্রামে আমরা ছোট্ট একটি HTTP সার্ভার চালু করি (port 0 মানে “যেকোনো খালি port দিন”), তারপর তার কাছে সত্যিকারের অনুরোধ পাঠাই — GET with query, দুটি POST (একটি সফল, একটি যাচাইয়ে ব্যর্থ), একটি 404, আর শেষে সার্ভার বন্ধ করে একটি network error:

const http = require("node:http");
const send = (res, status, data) =>
  res.writeHead(status, { "Content-Type": "application/json", Connection: "close" }).end(JSON.stringify(data));
const server = http.createServer(async (req, res) => {          // ছোট্ট সার্ভার
  const url = new URL(req.url, "http://127.0.0.1");
  if (req.method === "GET" && url.pathname === "/api/products")
    return send(res, 200, { category: url.searchParams.get("category"), page: +url.searchParams.get("page") });
  if (req.method !== "POST" || url.pathname !== "/api/orders") return send(res, 404, { error: "পাওয়া যায়নি" });
  let text = "";
  for await (const chunk of req) text += chunk;                // body টুকরো টুকরো আসে
  const order = JSON.parse(text);
  order.qty > 0 ? send(res, 201, { id: "A-101", ...order }) : send(res, 422, { error: "qty ০-এর বেশি হতে হবে" });
});
server.listen(0, "127.0.0.1", async () => {                     // client অংশ
  const base = `http://127.0.0.1:${server.address().port}`;
  const q = new URLSearchParams({ category: "rice", page: 2 });
  let res = await fetch(`${base}/api/products?${q}`);
  console.log(res.status, res.headers.get("content-type"), await res.json());
  for (const qty of [2, 0]) {
    res = await fetch(`${base}/api/orders`, { method: "POST",
      headers: { "Content-Type": "application/json" }, body: JSON.stringify({ productId: 7, qty }) });
    console.log(res.status, res.ok, await res.json());
  }
  res = await fetch(`${base}/api/products/999`);
  console.log(res.status, res.ok, await res.json());
  server.close(() => fetch(base).catch((e) => console.log(e.name, e.message, e.cause.code)));
});

আউটপুট পাঁচ লাইনের। 200 application/json { category: 'rice', page: 2 } — সার্ভার URL দিয়ে query পড়েছে, আর page এসেছিল string "2" হয়ে, + দিয়ে সংখ্যা বানানো হয়েছে। 201 true { id: 'A-101', productId: 7, qty: 2 } — সফল তৈরি। 422 false { error: 'qty ০-এর বেশি হতে হবে' } আর 404 false { error: 'পাওয়া যায়নি' } — দুটিতেই fetch reject হয়নি, শুধু ok হলো false. শেষ লাইন TypeError fetch failed ECONNREFUSED — সার্ভার বন্ধ, উত্তরই আসেনি, তাই এবার সত্যিই reject; Node আসল কারণ রাখে e.cause-এ (পর্ব ২৯-এর error cause). সার্ভারে Connection: close দেওয়া হয়েছে যাতে পুরোনো সংযোগ পুনর্ব্যবহারের কারণে শেষ লাইনটি কখনো অন্য রকম না আসে; port নম্বর ইচ্ছা করেই ছাপা হয়নি, কারণ সেটি প্রতিবার বদলায়।

FormData আর ফাইল আপলোড

প্রোফাইল ছবি বা NID-এর স্ক্যান পাঠাতে JSON সুবিধার নয় — binary ফাইলকে লেখায় বদলালে আকার প্রায় এক-তৃতীয়াংশ বাড়ে। এর জন্য আছে FormData, যা multipart/form-data ফরম্যাটে পাঠায় — HTML ফর্ম যেভাবে পাঠায়:

const form = document.querySelector("#profile-form");      // <input type="file" name="photo"> আছে
const data = new FormData(form);                            // ফর্মের সব ঘর একসাথে
data.append("userId", "12");

const res = await fetch("/api/profile/photo", {
  method: "POST",
  body: data,            // Content-Type নিজে লিখবেন না!
});
if (!res.ok) throw new HttpError(res.status, await res.text());

একটি সূক্ষ্ম কথা: FormData-র সাথে Content-Type header নিজে লিখবেন না। ব্রাউজার নিজে multipart/form-data; boundary=----… লেখে, যেখানে boundary হলো অংশগুলোর মাঝের বিভাজক চিহ্ন; আপনি হাতে লিখলে boundary বাদ পড়ে আর সার্ভার body ভাঙতে পারে না। ছোট সাধারণ ফর্মের জন্য URLSearchParams-কে body করলে যায় application/x-www-form-urlencoded. আপলোডের অগ্রগতি (৪০% … ৮০%) দেখানো fetch-এ এখনো সহজ নয় — এই একটি কাজে অনেকে আজও পুরোনো XMLHttpRequest-এর upload.onprogress ব্যবহার করেন (অংশ ৮)।

৭ · ব্যর্থতার রকমফের — network error, timeout, cookie আর CORS

বাস্তবের নেটওয়ার্ক অনির্ভরযোগ্য: লোডশেডিংয়ে রাউটার বন্ধ, মেট্রোরেলে সুড়ঙ্গে ঢুকে মোবাইল ডেটা নেই, সার্ভার ব্যস্ত। ভালো অ্যাপ আগে থেকেই জানে কোন ব্যর্থতা কেমন দেখায়:

ঘটনাfetch কী করেকীভাবে চিনবেনব্যবহারকারীকে
ইন্টারনেট নেই, DNS ব্যর্থ, সার্ভার বন্ধrejectTypeError“সংযোগ নেই — আবার চেষ্টা করুন”
CORS-এ আটকে গেছেrejectTypeError (কারণ শুধু Console-এ)ডেভেলপারের সমস্যা — সার্ভারে ঠিক করতে হবে
Timeout বা ব্যবহারকারী বাতিল করেছেনrejectTimeoutError / AbortError“অনেক সময় লাগছে” / কিছু না দেখানো
4xx / 5xxপূর্ণ (resolve)res.ok === falsestatus অনুযায়ী নির্দিষ্ট বার্তা
সফল, কিন্তু body ভাঙা JSONres.json() rejectSyntaxError“অপ্রত্যাশিত উত্তর”

Timeout — অনন্ত অপেক্ষা নয়

fetch-এর নিজের কোনো সময়সীমা নেই। সার্ভার উত্তর না দিলে ব্রাউজার বা অপারেটিং সিস্টেমের নিজস্ব সীমা (প্রায়ই কয়েক মিনিট) পর্যন্ত ব্যবহারকারী ঘূর্ণির দিকে তাকিয়ে থাকেন। আধুনিক সমাধান এক লাইনের: fetch(url, { signal: AbortSignal.timeout(8000) }) — ৮ সেকেন্ডে উত্তর না এলে Promise reject হয় TimeoutError নামের DOMException দিয়ে। এখানে AbortSignal হলো একটি “থামার সংকেত”, যা fetch-কে মাঝপথে বাতিল করতে পারে। ব্যবহারকারী অন্য পাতায় চলে গেলে বা নতুন করে সার্চ করলে পুরোনো অনুরোধ বাতিল করা, একাধিক সংকেত জোড়া দেওয়া আর বুদ্ধিমান retry — সবকিছু পর্ব ৩৬-এ।

Cookie আর credentials

লগইন করার পর সার্ভার সাধারণত একটি session cookie দেয় (cookie-র খুঁটিনাটি পর্ব ৪০-এ)। fetch cookie পাঠাবে কি না তা ঠিক করে credentials option: "same-origin" (ডিফল্ট — শুধু নিজের সাইটে পাঠায়), "include" (অন্য origin-এও পাঠায়, যেমন shop.example.com থেকে api.example.com), আর "omit" (কখনো না)। "include" দিলেও সার্ভারকে স্পষ্টভাবে অনুমতি দিতে হয়, নইলে ব্রাউজার উত্তর আটকে দেয় — যা আমাদের নিয়ে যায় CORS-এ।

CORS — এক অনুচ্ছেদে

ধরুন আপনি evil.example সাইটে ঢুকলেন, আর সেই পাতার JavaScript চুপিচুপি আপনার ব্যাংকের API-তে fetch করে আপনার cookie দিয়ে ব্যালান্স পড়ে নিল — ভয়ংকর। এটি ঠেকাতে ব্রাউজার same-origin policy মানে: একটি পাতার JavaScript সাধারণত শুধু নিজের origin-এর (protocol + host + port একই) উত্তর পড়তে পারে। CORS (Cross-Origin Resource Sharing) হলো এই নিয়মের নিয়ন্ত্রিত ব্যতিক্রম: সার্ভার উত্তরের header-এ (Access-Control-Allow-Origin) জানায় কোন origin তার উত্তর পড়তে পারবে; অনুমতি না থাকলে ব্রাউজার উত্তরটি JavaScript-এর হাতে দেয় না, আর fetch reject হয় একই সাধারণ TypeError দিয়ে। মনে রাখার দুটি কথা: CORS প্রয়োগ করে ব্রাউজার (Node.js বা curl-এ CORS নেই), আর সিদ্ধান্ত নেয় সার্ভার — তাই CORS error front-end কোড বদলে সারানো যায় না, এবং mode: "no-cors" দিলে কেবল একটি পড়া-যায়-না এমন “opaque” উত্তর পাবেন। Preflight, credentials-এর নিয়ম আর নিরাপদ কনফিগারেশন পর্ব ৪৩-এ।

৮ · বাস্তব অ্যাপে — loading অবস্থা, cache, গোপন key আর DevTools

UI-র চারটি অবস্থা

প্রতিটি fetch-নির্ভর অংশের অন্তত চারটি অবস্থা থাকে, আর ব্যবহারকারীকে প্রতিটি দেখাতে হয়: loading (spinner বা skeleton — “আনছি”), success (ডেটা), empty (সফল কিন্তু কিছু নেই — “এই ক্যাটাগরিতে এখন কোনো পণ্য নেই”, যা error নয়!), আর error (বোধগম্য বার্তা আর একটি “আবার চেষ্টা করুন” বোতাম)। AI-এর লেখা অনেক কোডে শুধু success অবস্থা থাকে; ধীর নেটওয়ার্কে পাতা ফাঁকা পড়ে থাকে আর ব্যবহারকারী বোতাম বারবার চাপেন — ফলে একই POST কয়েকবার যায়। তাই অনুরোধ চলার সময় বোতাম নিষ্ক্রিয় রাখুন। একটি আবহাওয়া বা মুদ্রার হার দেখানোর widget-এর জন্য ভাবনাটি এরকম:

function bnMessage(err) {
  if (err.name === "TimeoutError") return "সার্ভার সাড়া দিতে দেরি করছে, একটু পরে চেষ্টা করুন।";
  if (err instanceof HttpError) {
    if (err.status === 404) return "এই শহরের তথ্য পাওয়া যায়নি।";
    if (err.status === 429) return "খুব দ্রুত অনেকবার চাওয়া হয়েছে, এক মিনিট অপেক্ষা করুন।";
    if (err.status >= 500) return "আমাদের দিকে সমস্যা হয়েছে, আমরা দেখছি।";
    return "অনুরোধটি গ্রহণ করা যায়নি।";
  }
  return "ইন্টারনেট সংযোগ দেখে আবার চেষ্টা করুন।";   // network error
}

async function showRate(box, currency) {
  box.textContent = "লোড হচ্ছে…";                          // loading
  try {
    const url = new URL("/api/rates", location.origin);
    url.searchParams.set("base", currency);
    const data = await getJSON(url, { signal: AbortSignal.timeout(8000) });
    box.textContent = data.rate ? `১ ${currency} = ${data.rate} টাকা` : "এখন হার পাওয়া যাচ্ছে না।";
  } catch (err) {
    box.textContent = bnMessage(err);                       // error + retry বোতাম দেখান
  }
}

এখানে getJSON ধরে নেওয়া হয়েছে অংশ ৫-এর মতো, তবে দ্বিতীয় argument-এ options নিয়ে আসল fetch-এ পাঠায়। দেখুন, বার্তাগুলো ব্যবহারকারীর ভাষায়, কারিগরি শব্দ ছাড়া; আর আসল error-এর বিস্তারিত (status, body) পাঠানো উচিত আপনার log বা error monitoring ব্যবস্থায়, পর্দায় নয়।

Cache — একই জিনিস বারবার না আনা

পণ্যের তালিকা প্রতি সেকেন্ডে বদলায় না। সার্ভার উত্তরের সাথে Cache-Control header দিয়ে বলে দিতে পারে কতক্ষণ কপিটি জমিয়ে রাখা চলবে — যেমন Cache-Control: max-age=60 মানে “৬০ সেকেন্ড আমাকে আবার জিজ্ঞেস না করে এই কপিই ব্যবহার করুন”, আর no-store মানে “কখনো জমাবেন না” (ব্যাংকের তথ্যের জন্য)। সময় পেরোলে ব্রাউজার ETag দিয়ে জিজ্ঞেস করে “আমার কপি কি এখনো ঠিক?” — না বদলালে সার্ভার body ছাড়াই 304 Not Modified পাঠায়। এই cache ব্রাউজার নিজেই সামলায়, আপনার fetch কোড বদলাতে হয় না; চাইলে fetch(url, { cache: "no-store" }) দিয়ে প্রতি অনুরোধে নিয়ন্ত্রণ করা যায়। অফলাইনে চলা অ্যাপের জন্য নিজের হাতে cache সামলানো পর্ব ৪৪-এ।

গোপন API key কখনো front-end-এ নয়

Front-end-এর JavaScript পাঠানো হয় প্রতিটি ব্যবহারকারীর ব্রাউজারে — যে কেউ View Source বা DevTools-এর Network tab খুলে প্রতিটি লাইন আর প্রতিটি request-এর header দেখতে পারেন। তাই কোডে লেখা API key (পেমেন্ট গেটওয়ে, SMS সেবা, AI API, ডেটাবেসের পাসওয়ার্ড) কার্যত প্রকাশ্য। .env ফাইলে রাখলেও bundler (পর্ব ৪৬) সেটিকে কোডে বসিয়ে দেয় — VITE_ বা NEXT_PUBLIC_ উপসর্গওয়ালা মান ইচ্ছা করেই প্রকাশ্য। নিরাপদ পথ: গোপন key থাকবে শুধু আপনার নিজের সার্ভারে; ব্রাউজার আপনার সার্ভারকে ডাকে, আর সার্ভার key যোগ করে বাইরের সেবাকে ডাকে (একটি ছোট proxy endpoint). যেসব key প্রকাশ্য হওয়ার জন্যই বানানো (যেমন মানচিত্রের “publishable” key), সেগুলোতেও ডোমেইন-সীমা আর ব্যবহারের সীমা দিন। ভুল করে key প্রকাশ হয়ে গেলে সাথে সাথে সেটি বাতিল করে নতুন key নিন — git history থেকে মুছলেও পুরোনো কপি কোথাও থেকে যায়।

XMLHttpRequest আর axios — পুরোনো আর বিকল্প পথ

fetch-এর আগে ছিল XMLHttpRequest (XHR) — callback-ভিত্তিক, event-এ ভরা API, যা দিয়ে Ajax যুগ শুরু হয়েছিল (ভাবনার প্রশ্ন ২)। পুরোনো কোডে আজও দেখবেন:

// পুরোনো ধাঁচ — শুধু চেনার জন্য
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/products/7");
xhr.onload = () => {
  if (xhr.status === 200) console.log(JSON.parse(xhr.responseText));
};
xhr.onerror = () => console.log("network error");
xhr.send();

// axios (npm library) — একই কাজ
const { data } = await axios.get("/api/products/7");   // 4xx/5xx-এ নিজে থেকে reject করে

axios একটি জনপ্রিয় npm library (পর্ব ৪৬), যা ব্রাউজারে XHR আর Node-এ নিজস্ব HTTP ব্যবস্থার ওপর বানানো। এর সুবিধা: 2xx-এর বাইরে নিজে থেকেই reject করে, JSON নিজে parse করে, timeout আর “interceptor” (প্রতিটি অনুরোধে token যোগ করার মতো কাজ) সহজ, আপলোডের অগ্রগতি দেখায়। অসুবিধা: আলাদা একটি dependency, যা bundle-এর আকার বাড়ায়। আজ অনেক দল অংশ ৫-এর মতো ২০–৩০ লাইনের নিজস্ব fetch wrapper দিয়েই কাজ সারে।

Streaming — পুরো body-র জন্য অপেক্ষা না করা

res.json() পুরো body আসা পর্যন্ত অপেক্ষা করে। কিন্তু AI চ্যাটের উত্তর যেভাবে শব্দে শব্দে ভেসে ওঠে, বা বড় ফাইলের ডাউনলোডের অগ্রগতি — এসবের জন্য body-কে টুকরো ধরে পড়তে হয়। res.body একটি ReadableStream: const reader = res.body.getReader(), তারপর লুপে const { done, value } = await reader.read(); প্রতিটি value একটি byte-এর টুকরো (Uint8Array), যা TextDecoder দিয়ে লেখায় বদলানো যায়। ব্রাউজারে আর Node 20-এ for await (const chunk of res.body)-ও চলে (পর্ব ২৭-এর async iterator). লাইনে লাইনে JSON (পর্ব ১৯-এর NDJSON) বা Server-Sent Events (পর্ব ৫১) এভাবেই পড়া হয়।

DevTools-এর Network tab — আপনার সেরা বন্ধু

fetch-এর যেকোনো সমস্যায় প্রথম কাজ: DevTools (F12) খুলে Network tab, তারপর পাতায় কাজটি আবার করুন। প্রতিটি অনুরোধের একটি সারি দেখবেন — method, status, ধরন, আকার, সময়। সারিতে ক্লিক করলে Headers (কোন URL-এ কোন header গেল, কোন status এল), Payload (আপনার পাঠানো body — [object Object] দেখলেই বুঝবেন JSON.stringify ভুলেছেন), Response (সার্ভার আসলে কী পাঠাল) আর Timing. “Fetch/XHR” ফিল্টার শুধু API অনুরোধ দেখায়, “Throttling” দিয়ে ধীর 3G-র মতো নেটওয়ার্ক নকল করে loading অবস্থা পরীক্ষা করা যায়, আর “Offline” দিয়ে network error. সারিতে ডান-ক্লিক করে “Copy as fetch” দিলে পুরো অনুরোধটি কোড হিসেবে পাওয়া যায়।

C/C++/Python/Java জানলে

Python-এ requests library (আর async-এর জন্য httpx) প্রায় একই ধারণায় চলে, এবং সেখানেও 404-এ exception হয় না — r.raise_for_status() নিজে ডাকতে হয়, ঠিক res.ok যাচাইয়ের মতো (Python-এর HTTP ও API). Java-র java.net.http.HttpClient (Java 11 থেকে) sendAsync-এ CompletableFuture ফেরত দেয় — JavaScript-এর Promise-এর সমতুল্য — আর সেটিও status-এর জন্য throw করে না (Java Networking). C আর C++-এর standard library-তে HTTP নেই; সাধারণত libcurl বা Asio-ভিত্তিক library লাগে (C++ Networking). JavaScript-এর বিশেষত্ব হলো ব্রাউজারের নিরাপত্তা-স্তর: CORS, cookie-র নিয়ম আর নিষিদ্ধ header — যা ওই ভাষার server-side প্রোগ্রামে নেই।

পরীক্ষা ও ইন্টারভিউতে
  • fetch কি 404-এ reject করে? — না; Promise পূর্ণ হয় res.ok === false দিয়ে। Reject হয় শুধু network ব্যর্থতা, CORS আর abort-এ।
  • GET আর POST-এর পার্থক্য? — GET পড়ার জন্য, ডেটা যায় URL-এর query-তে, safe ও idempotent; POST নতুন কিছু তৈরি বা কাজ চালায়, ডেটা যায় body-তে, idempotent নয়।
  • PUT আর PATCH? — PUT পুরো resource বদলে বসায় (idempotent); PATCH শুধু দেওয়া অংশ বদলায়।
  • 401 আর 403-এর পার্থক্য? — 401: পরিচয় প্রমাণ হয়নি (লগইন লাগবে); 403: পরিচয় জানা, কিন্তু অনুমতি নেই।
  • CORS error কে ঠিক করে? — সার্ভার, Access-Control-Allow-Origin-এর মতো header দিয়ে; নিয়মটি প্রয়োগ করে ব্রাউজার।

৯ · AI-এর যুগে — fetch কোডে AI-এর চেনা ভুল

“একটি API থেকে পণ্য এনে দেখাও” — AI এক সেকেন্ডে কোড দেবে, আর ডেমোতে সেটি নিখুঁত চলবে। কারণ ডেমোতে নেটওয়ার্ক ভালো, সার্ভার সুস্থ, ডেটা ঠিক। সমস্যা আসে প্রোডাকশনে, প্রথম 500, প্রথম ধীর মোবাইল সংযোগ বা প্রথম দুষ্টু ইনপুটে। AI-এর লেখা fetch কোডে বারবার যে ভুলগুলো দেখা যায়:

AI যা প্রায়ই লেখেসমস্যাযা চাইবেন
await (await fetch(url)).json()res.ok দেখা নেই — error body-কে ডেটা ভাবে, বা HTML-এ SyntaxErrorbody পড়ার আগে res.ok, নইলে HttpError
`/search?q=${text}`বাংলা, &, # query ভেঙে দেয়; বাড়তি শর্ত ঢোকানো যায়URLSearchParams / encodeURIComponent
const API_KEY = "sk-…" front-end-এযে কেউ key দেখে ব্যবহার করতে পারে — বিল আপনারkey শুধু সার্ভারে, ব্রাউজার ডাকে নিজের proxy
timeout নেইধীর সার্ভারে অনন্ত spinnerAbortSignal.timeout(...)
body: order বা Content-Type ছাড়া[object Object] যায়, বা সার্ভার body পড়ে নাJSON.stringify + Content-Type: application/json
শুধু success-এর UI, catch-এ console.logব্যবহারকারী কিছুই জানেন না; বোতাম বারবার চাপা → দুটি অর্ডারloading/empty/error অবস্থা, বোতাম নিষ্ক্রিয়, retry

AI-কে fetch-এর কোড লিখতে বললে ঝুঁকিগুলো আগেই শর্ত হিসেবে দিন, আর পরীক্ষার কেসও চেয়ে নিন:

আমি একটি অনলাইন মুদি দোকানের ব্রাউজার-কোড লিখছি (আধুনিক JavaScript, কোনো library নয়)। নিচের শর্ত মেনে একটি api(path, options) helper আর loadProducts(category, page) ফাংশন লিখে দাও: ১. fetch-এর পর body পড়ার আগে res.ok যাচাই করবে; ব্যর্থ হলে status আর সার্ভারের body (Content-Type দেখে json বা text) সহ HttpError class-এর error ছুড়বে। ২. 204 উত্তরে null ফেরত দেবে; সফল উত্তরে res.json(). ৩. URL বানাবে new URL আর URLSearchParams দিয়ে; কোথাও string জোড়া দিয়ে query বানাবে না (খোঁজার শব্দে বাংলা আর & থাকতে পারে)। ৪. প্রতিটি অনুরোধে AbortSignal.timeout(8000); বাইরে থেকে signal দিলে সেটিও মানবে। ৫. POST-এ JSON.stringify আর Content-Type: application/json; কোনো API key বা গোপন তথ্য কোডে রাখবে না। ৬. network error, TimeoutError আর HttpError-কে আলাদা করে বাংলায় ব্যবহারকারী-বান্ধব বার্তা দেবে। শেষে ৫টি পরীক্ষার কেস দাও: 404-এ JSON body, 502-এ HTML body, 204, সার্ভার বন্ধ (network error), আর "চা & দই" দিয়ে সার্চ — প্রতিটির প্রত্যাশিত ফলসহ।

AI-এর লেখা fetch কোড — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১এক method-এর HTTP/0.9 থেকে QUIC-এর ওপর চলা HTTP/3 — ত্রিশ বছরে HTTP কেন তিনবার নতুন করে গড়তে হলো, অথচ fetch কোড প্রায় একই রইল?

১৯৯১ সালে Tim Berners-Lee-র প্রথম HTTP ছিল প্রায় হাস্যকর রকম সরল: client একটি TCP সংযোগ খুলে এক লাইন পাঠাত — GET /page.html — আর সার্ভার HTML পাঠিয়ে সংযোগ বন্ধ করত। কোনো header নেই, status code নেই, ছবি বা অন্য ফরম্যাটের ধারণা নেই। পরে এটির নাম দেওয়া হয় HTTP/0.9. ওয়েব দ্রুত বড় হলো, তাই ১৯৯৬ সালে RFC 1945 HTTP/1.0-কে লিখিত রূপ দিল — header, status code, Content-Type, POST. সমস্যা থেকে গেল: প্রতিটি ফাইলের জন্য নতুন TCP সংযোগ, আর প্রতিটি সংযোগ খুলতে সময় লাগে।

১৯৯৭ সালে HTTP/1.1 (RFC 2068, পরে ১৯৯৯ সালে RFC 2616) সংযোগ খোলা রেখে পুনর্ব্যবহার (keep-alive) চালু করল, Host header বাধ্যতামূলক করল যাতে এক IP-তে অনেক ওয়েবসাইট চলতে পারে, আর cache-এর নিয়ম গোছাল। এই সংস্করণটি প্রায় ষোলো বছর ওয়েব চালিয়েছে। কিন্তু এর একটি মৌলিক দুর্বলতা ছিল — একটি সংযোগে উত্তর আসে পরপর, একটি ধীর উত্তর পেছনেরগুলোকে আটকে রাখে (head-of-line blocking). ব্রাউজাররা তাই একটি সাইটে একসাথে ছয়টির মতো সংযোগ খুলত, আর ডেভেলপাররা ছবি জোড়া লাগানো (sprite) আর ফাইল একত্র করার মতো কৌশল করত।

Google ২০০৯ সালে SPDY নামে পরীক্ষা শুরু করে, যার ভিত্তিতে ২০১৫ সালে আসে HTTP/2 (RFC 7540): লেখার বদলে binary frame, এক সংযোগে বহু অনুরোধ একসাথে (multiplexing), আর header সংকোচন। তবু নিচে TCP থাকায় একটি প্যাকেট হারালে পুরো সংযোগ অপেক্ষা করত — মোবাইল নেটওয়ার্কে যা প্রায়ই ঘটে। সমাধান এল আরও নিচের স্তরে: Google-এর শুরু করা QUIC, যা UDP-র ওপর নিজস্ব নির্ভরযোগ্যতা আর এনক্রিপশন বানায়, প্রতিটি stream আলাদাভাবে চলে। ২০২১ সালে QUIC (RFC 9000) আর ২০২২ সালে HTTP/3 (RFC 9114) মানদণ্ড হয়।

এত বদলের পরও আপনার fetch(url, { method, headers, body }) একই রয়ে গেছে, আর আপনি জানতেও পারেন না কোন সংস্করণ চলছে (DevTools-এর Network tab-এ “Protocol” কলাম চালু করলে দেখবেন h2 বা h3). কারণ HTTP-র অর্থ — method, status, header, body — কখনো বদলায়নি; বদলেছে শুধু তারে কীভাবে পাঠানো হয়। আজকের মানদণ্ড (RFC 9110, ২০২২) এই অর্থটিকে আলাদা দলিলে লিখে রেখেছে, সংস্করণ-নিরপেক্ষভাবে।

মূল উপলব্ধি: ভালো protocol অর্থ (semantics) আর পরিবহন (wire format)-কে আলাদা রাখে। তাই পরিবহন তিনবার নতুন হলেও ওপরের প্রোগ্রামগুলো ভাঙেনি — আপনার API নকশাতেও এই বিভাজন রাখার চেষ্টা করুন।

প্র ০২যে XMLHttpRequest আজ “পুরোনো” বলে এড়ানো হয়, তার জন্ম Microsoft-এর একটি ইমেইল প্রোডাক্টে — এটি কীভাবে পুরো ওয়েবকে বদলে দিল, আর “Ajax” নামটি কোথা থেকে এল?

১৯৯০-এর দশকের শেষে Microsoft-এর Exchange দল Outlook-এর একটি ওয়েব সংস্করণ বানাচ্ছিল — Outlook Web Access. সমস্যা ছিল: নতুন ইমেইল এসেছে কি না দেখতে বা একটি ফোল্ডার খুলতে পুরো পাতা আবার লোড করতে হতো, যা ডেস্কটপ Outlook-এর তুলনায় ধীর আর বিরক্তিকর। দলটি ব্রাউজারের ভেতর থেকে পেছনে সার্ভারে HTTP অনুরোধ পাঠানোর একটি component বানাল। ১৯৯৯ সালে Internet Explorer 5-এর সাথে সেটি MSXML library-র ভেতরে ActiveX object হিসেবে আসে — নাম Microsoft.XMLHTTP. নামে XML থাকার কারণ অনেকটা ব্যবসায়িক: library-টি XML-এর জন্য ছিল, আর সেই পথেই এটি ব্রাউজারে জায়গা পায়। বাস্তবে এটি দিয়ে যেকোনো লেখা আনা যেত।

কয়েক বছর জিনিসটি প্রায় অজানা ছিল। ২০০২ সালের দিকে Mozilla একই ইন্টারফেস নিজস্ব XMLHttpRequest object হিসেবে যোগ করে, পরে Safari আর Opera-ও। তারপর এল দুটি প্রোডাক্ট যা সবার চোখ খুলে দিল: ২০০৪ সালে Gmail — পাতা না বদলে ইমেইল খোলা, পাঠানো, খোঁজা — আর ২০০৫ সালে Google Maps, যেখানে মানচিত্র টেনে সরালে নতুন টুকরো পেছনে নিঃশব্দে চলে আসত। মানুষ প্রথমবার বুঝল ব্রাউজারের পাতা ডেস্কটপ অ্যাপের মতো আচরণ করতে পারে।

২০০৫ সালের ফেব্রুয়ারিতে Adaptive Path-এর Jesse James Garrett “Ajax: A New Approach to Web Applications” নামে একটি প্রবন্ধে এই কৌশলের নাম দেন — Asynchronous JavaScript and XML. নামটি জাদুর মতো কাজ করল: আলাদা আলাদা কৌশলগুলো (XHR, DOM বদলানো, JavaScript) একটি পরিচিত শব্দ পেল, যা নিয়ে ম্যানেজার আর ডেভেলপার একসাথে কথা বলতে পারেন। jQuery-র $.ajax (২০০৬) ব্রাউজারভেদের ঝামেলা লুকিয়ে কৌশলটিকে সবার হাতে পৌঁছে দেয়। পরে W3C আর WHATWG XHR-কে মানদণ্ডে লেখে।

তাহলে আজ XHR “পুরোনো” কেন? এর API event আর callback-নির্ভর (onload, onerror, onreadystatechange-এর পাঁচটি অবস্থা), Promise-এর আগের যুগের। request, response, header-এর আলাদা object নেই, সবকিছু এক object-এ গাদা। তবু এটি মুছে যায়নি — আপলোডের অগ্রগতি দেখানো এখনো XHR-এ সহজ, আর axios ব্রাউজারে এর ওপরই চলে। আর মজার বিষয়: “Ajax”-এর X (XML) আজ প্রায় কেউ ব্যবহার করে না — জায়গা নিয়েছে JSON.

মূল উপলব্ধি: প্রযুক্তি জনপ্রিয় হয় তখন, যখন একটি চোখধাঁধানো প্রোডাক্ট তার সম্ভাবনা দেখায় আর একটি সহজ নাম তাকে আলোচনাযোগ্য করে। XHR ছয় বছর অপেক্ষা করেছে Gmail আর “Ajax” শব্দটির জন্য।

প্র ০৩Roy Fielding-এর ২০০০ সালের ডক্টরাল থিসিস থেকে আসা “REST” আসলে কী বলেছিল — আর আজ যাকে “REST API” বলা হয়, তা কতটা তার সাথে মেলে?

Roy Fielding ছিলেন ওয়েবের ভেতরের মানুষ — HTTP/1.1 মানদণ্ডের অন্যতম লেখক আর Apache HTTP Server প্রকল্পের প্রতিষ্ঠাতাদের একজন। ২০০০ সালে University of California, Irvine-এ তাঁর ডক্টরাল থিসিস “Architectural Styles and the Design of Network-based Software Architectures”-এর পঞ্চম অধ্যায়ে তিনি একটি প্রশ্নের উত্তর খোঁজেন: ওয়েব এত বড় হয়েও কেন ভেঙে পড়ল না? তাঁর উত্তর ছিল কয়েকটি নকশা-শর্তের একটি সমষ্টি, যার নাম দিলেন REST — Representational State Transfer.

শর্তগুলো মোটামুটি এরকম: client আর server আলাদা; প্রতিটি অনুরোধ নিজেই সম্পূর্ণ (stateless) — সার্ভার আগের অনুরোধ মনে রাখে না, তাই যেকোনো সার্ভার যেকোনো অনুরোধ সামলাতে পারে; উত্তর cache করা যায় কি না স্পষ্ট বলা থাকে; মাঝখানে proxy বা CDN-এর মতো স্তর বসানো যায় (layered system); আর সবচেয়ে গুরুত্বপূর্ণ — একটি অভিন্ন ইন্টারফেস (uniform interface): সবকিছু resource, প্রতিটির ঠিকানা (URI), আর অল্প কয়েকটি সাধারণ method দিয়ে তাদের representation (যেমন JSON বা HTML) আদান-প্রদান। এর একটি অংশ ছিল “hypermedia as the engine of application state” (HATEOAS): উত্তরের ভেতরেই পরের সম্ভাব্য কাজের link থাকবে, ঠিক যেমন HTML পাতায় link থাকে, তাই client-কে আগে থেকে সব URL জানতে হবে না।

আজ যাকে সাধারণত “REST API” বলা হয়, তা মূলত এর একটি অংশ মানে: নামপদ-ভিত্তিক URL, HTTP method-এর সঠিক ব্যবহার, status code, JSON. HATEOAS প্রায় কেউ মানে না — client-এর কোডে URL লেখা থাকে। Fielding নিজে ২০০৮ সালে একটি ব্লগ লেখায় বিরক্তি প্রকাশ করেছিলেন যে hypertext ছাড়া API-কে REST বলা উচিত নয়। তাই অনেকে আজকের ধরনকে “RESTful” বা “HTTP JSON API” বলেন।

তবু Fielding-এর শর্তগুলোর ব্যবহারিক মূল্য বিশাল, আর এই পর্বের অনেক কিছুই তার সরাসরি ফল। GET safe আর PUT idempotent বলেই ব্রাউজার, cache আর retry-লজিক নিরাপদে সিদ্ধান্ত নিতে পারে; stateless বলেই একটি API-র পেছনে দশটি সার্ভার বসিয়ে load ভাগ করা যায়; Cache-Control আছে বলেই CDN জনপ্রিয় পণ্যের তালিকা হাজার জনকে মূল সার্ভার না ছুঁয়েই দিতে পারে। GraphQL বা gRPC-র মতো বিকল্প এসেছে নির্দিষ্ট সমস্যার জন্য, কিন্তু সাধারণ ওয়েব API-র ডিফল্ট আজও এই ধাঁচ।

মূল উপলব্ধি: REST কোনো library বা ফরম্যাট নয় — এটি এমন কিছু শর্ত, যা মানলে সিস্টেম বড় হলেও টেকে। নিয়মের পেছনের “কেন” বুঝলে কখন নিয়ম ভাঙা চলে, সেটাও বোঝা যায়।

প্র ০৪ব্রাউজারে XHR থাকতে fetch কেন দরকার হলো, আর WHATWG-এর Fetch মানদণ্ড শুধু একটি function-এর চেয়ে বড় কেন? Node.js-এ fetch আসতে এত বছর লাগল কেন?

২০১০-এর দশকের শুরুতে ব্রাউজারের ভেতরে “কিছু আনা”র কাজ ছড়িয়ে ছিল অনেক জায়গায়: <img>, <script>, CSS-এর ফন্ট, XHR, form জমা, পাতা খোলা। প্রতিটির নিজস্ব নিয়ম ছিল — কোনটি cookie পাঠাবে, কোনটি redirect মানবে, CORS কোথায় প্রযোজ্য — আর এসব নিয়ম লেখা ছিল নানা দলিলে, কখনো একে অপরের সাথে বিরোধে। WHATWG-এ Anne van Kesteren-এর নেতৃত্বে Fetch Standard এই সব কাজের জন্য একটি একক মডেল লেখে: request কী, response কী, redirect, CORS, credentials, cache mode — সবকিছুর একটি সাধারণ সংজ্ঞা। আজ ব্রাউজারে একটি ছবি লোড হওয়াও আসলে সেই “fetch” প্রক্রিয়ারই একটি রূপ।

এই নীচের মডেলের ওপর JavaScript-এর জন্য বানানো হলো fetch() function আর Request, Response, Headers class. লক্ষ্য ছিল কয়েকটি: Promise-ভিত্তিক API (ES2015-এর Promise ঠিক তখনই আসছিল), body-কে stream হিসেবে দেখা, আর এমন object যা service worker-এ (পর্ব ৪৪) অনুরোধ আটকে নিজে Response বানিয়ে ফেরত দিতে পারে — অফলাইন অ্যাপের ভিত্তি। এই পর্বের কোড-বক্সে আমরা যে new Response(...) দিয়ে নকল সার্ভার বানালাম, সেই ক্ষমতাটিও এই নকশার অংশ। Chrome 42 আর Firefox 39-এ ২০১৫ সালে fetch আসে, Safari-তে ২০১৭ সালে।

Node.js-এর গল্প আলাদা। Node-এর নিজস্ব http module ছিল ২০০৯ থেকে, আর npm-এ request, node-fetch, axios-এর মতো library ছিল। ব্রাউজারের fetch-এর অনেক ধারণা (CORS, cookie jar, origin) সার্ভারে অর্থহীন, আর ব্রাউজারের stream-এর API Node-এর নিজস্ব stream থেকে আলাদা ছিল। কয়েক বছর বিতর্কের পর Node-এর দল নতুন করে লেখা undici HTTP client-এর ওপর fetch বানাল। Node 17.5-এ এটি একটি flag-এর পেছনে আসে, আর ২০২২ সালের এপ্রিলে Node 18-এ flag ছাড়াই global fetch হিসেবে (তখনো “experimental” লেবেলসহ; Node 21-এ লেবেলটি সরে যায়)। ফলে আজ একই fetch কোড ব্রাউজার, Node, Deno আর Bun-এ চলে — এই পর্বের Node উদাহরণটিই তার প্রমাণ। তবে পার্থক্যও আছে: Node-এর error বার্তা fetch failed, আর আসল কারণ থাকে cause-এ।

মূল উপলব্ধি: সবচেয়ে টেকসই API প্রায়ই একটি বড় একীকরণের ফল — অনেক ছড়ানো নিয়মকে একটি মডেলে আনা। fetch-এর জাদু function-টিতে নয়, তার নিচের সেই অভিন্ন মডেলে, যা ব্রাউজার থেকে সার্ভারে পর্যন্ত এক ভাষা দিয়েছে।

প্র ০৫fetch কেন 404 বা 500-এ reject করে না? এটি কি নকশার ভুল, নাকি সচেতন সিদ্ধান্ত — আর axios কেন উল্টো পথে গেল?

প্রথমবার শুনলে এটি ভুলের মতোই শোনায়: সার্ভার বলছে “পাওয়া যায়নি”, অথচ Promise বলছে “সফল”! কিন্তু fetch-এর নকশাকারীদের যুক্তি পরিষ্কার। fetch-এর কাজ হলো একটি HTTP আদান-প্রদান সম্পন্ন করা। সার্ভার যদি একটি বৈধ HTTP উত্তর পাঠায় — যেকোনো status-সহ — তবে পরিবহনের দিক থেকে কাজটি সফল: অনুরোধ পৌঁছেছে, উত্তর এসেছে, header পড়া গেছে, body পড়া যাবে। কোন status “ভুল” তা নির্ভর করে প্রসঙ্গের ওপর, আর সেই প্রসঙ্গ জানে কেবল আপনার অ্যাপ।

উদাহরণ ভাবুন। “এই ইমেইল দিয়ে আগে নিবন্ধন হয়েছে কি?” — জানতে GET /api/users?email=…; এখানে 404 মানে “না, নতুন”, যা একটি সম্পূর্ণ স্বাভাবিক, প্রত্যাশিত ফল। Cache যাচাইয়ে 304 একটি সুখবর। একটি health-check পাতা 503 পেলে সেটাই রিপোর্ট করতে চায়। 401-এ আপনি token নবায়ন করে আবার চেষ্টা করতে চান। fetch যদি এদের সবার জন্য reject করত, তাহলে প্রত্যাশিত ফলের জন্যও exception-এর পথ নিতে হতো, আর “উত্তরই আসেনি” (যেখানে retry বা অফলাইন বার্তা দরকার) আর “উত্তর এসেছে, খবর খারাপ” (যেখানে status দেখে সিদ্ধান্ত) — এই দুই সম্পূর্ণ ভিন্ন অবস্থা এক catch-এ মিশে যেত।

নকশাটিতে আরও একটি স্তর আছে: reject-এর সময় fetch ইচ্ছা করেই কারণ জানায় না — CORS ব্যর্থতা আর সংযোগ ব্যর্থতা একই সাধারণ TypeError. কারণ বিস্তারিত জানালে একটি দুষ্টু পাতা অন্য সাইট বা ব্যবহারকারীর স্থানীয় নেটওয়ার্ক সম্পর্কে তথ্য বের করতে পারত। তাই “reject” মানে কেবল “কোনো ব্যবহারযোগ্য HTTP উত্তর পাওয়া যায়নি”।

axios ভিন্ন দর্শন নিয়েছে: ডিফল্টে 2xx-এর বাইরে সব reject (আর validateStatus option দিয়ে বদলানো যায়)। এটি “অ্যাপ-ডেভেলপারের সুবিধা” বেছে নিয়েছে — বেশিরভাগ ক্ষেত্রে ২০০ ছাড়া সব আসলেই ব্যর্থতা, আর ভুলে যাচাই বাদ পড়ার ঝুঁকি কমে। দুটিই যুক্তিসংগত; fetch একটি নিচু স্তরের, সাধারণ-উদ্দেশ্যের primitive, axios একটি উঁচু স্তরের সুবিধাজনক library. এজন্যই পেশাদার কোডবেসে fetch-এর ওপর একটি পাতলা helper (এই পর্বের getJSON/api) প্রায় সবসময় থাকে — নিচু স্তরের সততা আর উঁচু স্তরের সুবিধা, দুটোই।

মূল উপলব্ধি: “ব্যর্থতা” কোন স্তরে সংজ্ঞায়িত, তা স্পষ্ট রাখা একটি নকশার সিদ্ধান্ত। নিচু স্তর সৎভাবে যা ঘটেছে তা জানায়; কোনটি অ্যাপের জন্য ভুল, তা ঠিক করে ওপরের স্তর।

অনুশীলন

বেশিরভাগ কাজ ভাবনার আর DevTools-এ দেখার — ফল আগে অনুমান করুন, তারপর মিলিয়ে নিন।

  1. Status বেছে নিন: প্রতিটি ঘটনায় সার্ভারের কোন status দেওয়া উচিত? (ক) নতুন অর্ডার সফলভাবে তৈরি (খ) লগইন ছাড়া “আমার অর্ডার” চাওয়া (গ) সাধারণ ক্রেতা অ্যাডমিনের রিপোর্ট চাইলেন (ঘ) ফোন নম্বরে ১০টি অঙ্ক (ঙ) কার্ট থেকে item মুছে ফেলা হলো, ফেরত দেওয়ার কিছু নেই (চ) এক মিনিটে ২০০টি অনুরোধ।

    (ক) 201 Created, সাথে Location header. (খ) 401 — পরিচয় নেই। (গ) 403 — পরিচয় জানা, অনুমতি নেই। (ঘ) 422 — JSON-এর গঠন ঠিক, মান অগ্রহণযোগ্য (অনেক API এখানে 400-ও ব্যবহার করে; গুরুত্বপূর্ণ হলো body-তে field-ধরে বার্তা)। (ঙ) 204 No Content — client-এ res.json() ডাকবেন না। (চ) 429 Too Many Requests, সাথে Retry-After. খেয়াল করুন, এর একটিতেও fetch reject করবে না — সবকটিই আপনার res.ok/status যাচাইয়ের দায়িত্ব।

  2. বাগ খুঁজুন: এই কোডে অন্তত চারটি সমস্যা আছে। কী কী, আর ঠিক রূপ কী? const r = await fetch("/api/search?q=" + input.value);
    const items = await r.json();
    await fetch("/api/orders", { method: "POST", body: { id: items[0].id } });

    (১) Query হাতে জোড়া — বাংলা, &, # ভাঙবে; লিখুন new URLSearchParams({ q: input.value }). (২) r.ok দেখা নেই — 404/500-এ error body-কে ডেটা ধরবে বা HTML-এ SyntaxError. (৩) Body একটি object — যাবে "[object Object]"; দরকার JSON.stringify(...) আর Content-Type: application/json. (৪) দ্বিতীয় fetch-এর ফলাফলও যাচাই হয়নি — 422 এলে ব্যবহারকারী জানবেন না। বাড়তি: items খালি হলে items[0].id TypeError (empty অবস্থা!), কোনো timeout নেই, আর অর্ডার চলার সময় বোতাম নিষ্ক্রিয় করা হয়নি — দুবার চাপলে দুটি অর্ডার। সব মিলিয়ে, অংশ ৫-এর getJSON আর একটি postJSON helper দিয়ে লিখলে প্রায় সব সমস্যা এক জায়গায় সমাধান হয়।

  3. postJSON লিখুন (কোড): অংশ ৫-এর getJSON-এর ধাঁচে postJSON(url, data, { timeout = 8000 } = {}) লিখুন, যা JSON পাঠায়, 204-এ null দেয়, ব্যর্থতায় HttpError ছোড়ে আর timeout মানে। Node 20-এ উপরের ছোট্ট সার্ভারের সাথে চালিয়ে qty: 2 আর qty: 0 দুটোই পরীক্ষা করুন।

    একটি সমাধান: async function postJSON(url, data, { timeout = 8000 } = {}) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(data), signal: AbortSignal.timeout(timeout) }); if (res.status === 204) return null; const isJSON = res.headers.get("content-type")?.includes("json"); const body = isJSON ? await res.json() : await res.text(); if (!res.ok) throw new HttpError(res.status, body); return body; }. qty: 2-এ ফেরত আসে { id: 'A-101', productId: 7, qty: 2 }; qty: 0-এ HttpError, যার status ৪২২ আর body.error সার্ভারের বাংলা বার্তা। ভাবুন: এই ফাংশন কি নিজে থেকে retry করবে? POST idempotent নয় — তাই না, অন্তত Idempotency-Key ছাড়া নয়।

  4. DevTools অভিযান: যেকোনো পরিচিত ওয়েবসাইট (খবর, ই-কমার্স) খুলে F12 → Network → “Fetch/XHR” ফিল্টার দিন, তারপর পাতা স্ক্রল বা সার্চ করুন। তিনটি অনুরোধ বেছে লিখুন: method, status, Content-Type, আর response কি JSON? তারপর Throttling “Slow 3G” আর পরে “Offline” দিয়ে একই কাজ করুন — সাইটটি loading আর error অবস্থা কীভাবে দেখায়?

    সাধারণত দেখবেন বেশিরভাগ অনুরোধ GET, status 200, Content-Type: application/json; কিছু 304 (cache যাচাই), আর analytics-এর জন্য কিছু POST বা 204. Response tab-এ JSON-এর গঠন দেখে বুঝবেন পাতার কোন অংশ কোন API থেকে আসে। Query string-এ বাংলা সার্চ করলে %E0%A6… ধরনের encode দেখবেন। Slow 3G-তে ভালো সাইট skeleton বা spinner দেখায়; Offline-এ কেউ “সংযোগ নেই” ব্যানার দেখায়, কেউ শুধু ফাঁকা থাকে — দ্বিতীয়টি ঠিক সেই ভুল, যা এই পর্বে এড়াতে শিখলেন। Headers tab-এ cache-control আর access-control-allow-origin খুঁজে দেখুন — সার্ভার cache আর CORS নিয়ে কী সিদ্ধান্ত নিয়েছে, সরাসরি পড়া যায়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমব্রাউজারে const data = await getJSON(url) চালালে যা যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. fetch একটি pending Promise ফেরত দেয়, আর ব্রাউজার request পাঠায়
  2. Status লাইন আর header পৌঁছায় — Promise একটি Response দিয়ে পূর্ণ হয়
  3. res.ok যাচাই করা হয় — ব্যর্থ হলে HttpError
  4. res.json() দ্বিতীয় Promise দিয়ে body পড়ে parse করে
  5. parse করা ডেটা data-তে আসে, আর res.bodyUsed হয় true

সার্ভার 404 Not Found ফেরত দিলে await fetch(url)-এর কী হয়?

  • Promise একটি TypeError দিয়ে reject হয়
  • Promise একটি Response দিয়ে পূর্ণ হয়, যার ok হলো false
  • Promise null দিয়ে পূর্ণ হয়
  • fetch নিজে থেকে আবার চেষ্টা করে

একটি Response-এ প্রথমে await res.json() সফল হলো। এরপর একই object-এ await res.text() ডাকলে কী হবে?

  • একই JSON লেখাটি আবার পাওয়া যাবে
  • খালি string "" পাওয়া যাবে
  • ব্রাউজার অনুরোধটি আবার পাঠিয়ে নতুন body আনবে
  • TypeError দিয়ে reject হবে, কারণ body আগেই পড়া হয়েছে

সার্চের লেখা চা & দই query string-এ পাঠানোর সবচেয়ে নিরাপদ উপায় কোনটি?

  • new URLSearchParams({ q: text }) দিয়ে query বানানো
  • "/search?q=" + text
  • "/search?q=" + JSON.stringify(text)
  • `/search?q=${text}`

নিচের কোন HTTP method idempotent নয়?

  • GET
  • PUT
  • POST
  • DELETE

fetch দিয়ে JSON POST করার সময় body আর header কেমন হওয়া উচিত?

  • body: order, কোনো header লাগে না
  • body: JSON.stringify(order) আর Content-Type: application/json
  • body: order আর Accept: application/json
  • body: JSON.stringify(order) আর Content-Type: multipart/form-data

ব্রাউজারে একটি fetch CORS-এর কারণে আটকে গেলে কোনটি সত্য?

  • Node.js থেকে একই অনুরোধও একইভাবে আটকে যাবে
  • Promise একটি Response দিয়ে পূর্ণ হয়, যার status 403
  • mode: "no-cors" দিলেই উত্তর পড়া যাবে
  • সমাধান সার্ভারে — উত্তরের header দিয়ে অনুমতি দিতে হয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. HTTP = request (method, URL, header, body) → response (status, header, body); GET পড়ে, POST তৈরি করে, PUT/DELETE idempotent.
  2. fetch-এর দুই ধাপ: header এলে Response, তারপর json()/text()/blob() দিয়ে body — আর body পড়া যায় একবারই.
  3. 404/500-এ fetch reject করে না; body পড়ার আগে res.ok দেখে HttpError ছুড়ুন। Reject শুধু network, CORS, abort/timeout-এ।
  4. Query বানান URLSearchParams দিয়ে; POST-এ JSON.stringify + Content-Type; FormData-তে Content-Type নিজে লিখবেন না।
  5. বাস্তব অ্যাপে: AbortSignal.timeout, loading/empty/error অবস্থা, Cache-Control, CORS-এর সিদ্ধান্ত সার্ভারে — আর গোপন key কখনো front-end-এ নয়.

পরের পর্বে: ব্যবহারকারী “চ”, “চা”, “চাল” টাইপ করলেন — তিনটি অনুরোধ গেল, আর পুরোনোটির উত্তর এল সবার শেষে, ভুল ফল দেখিয়ে! AbortController দিয়ে অনুরোধ বাতিল, বুদ্ধিমান retry আর backoff, debounce আর throttle — asynchronous কোডকে বাস্তবের জন্য শক্ত করার কৌশল।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান