অধ্যায় 3 · HTTP ও ওয়েব API
ওয়েব কীভাবে কাজ করে: ক্লায়েন্ট, সার্ভার ও HTTP
- পৃষ্ঠা 11 / 22
- 16 মিনিট পড়া
আপনার Python কোড যখনই "একটা LLM কল করে", তখন আসলে সে নেটওয়ার্ক কানেকশনের ভেতর দিয়ে কয়েকশো বাইট টেক্সট অন্য কারও কম্পিউটারে পাঠায়, আর ফিরে আসা টেক্সটটা পড়ে। এই টেক্সট মেনে চলে কিছু নিয়ম, যার নাম HTTP। OpenAI আর Anthropic-এর SDK, চ্যাটবটের ওয়েব পেজ যখন তার ব্যাকএন্ডের সাথে কথা বলে, মডেল সার্ভার যখন প্রেডিকশন ফেরত দেয়, GitHub থেকে আসা একটা ওয়েবহুক: এদের সবার নিচে আছে HTTP রিকোয়েস্ট (request) আর রেসপন্স (response)।
কিছু গোলমাল হলে (401, টাইমআউট, 429 "too many requests", কিংবা এমন রেসপন্স যা JSON নয়) সবচেয়ে দ্রুত ঠিক করতে পারেন তাঁরা, যাঁরা এই কথোপকথনটা চোখের সামনে কল্পনা করতে পারেন। এই পাতা সেই ছবিটাই তৈরি করবে: কে কার সাথে কথা বলে, একটা HTTP মেসেজ আসলে দেখতে কেমন, একটা URL কীভাবে গঠিত, api.openai.com-এর মতো একটা নাম কীভাবে একটা কম্পিউটারে পৌঁছায়, HTTPS কী যোগ করে, আর curl -v ও ব্রাউজারের DevTools দিয়ে আসল রিকোয়েস্ট কীভাবে দেখা যায়।
যা শিখবেন
- ইন্টারনেট আর ওয়েবের পার্থক্য, আর ক্লায়েন্ট (client) ও সার্ভার (server) কী।
- একটা HTTP রিকোয়েস্ট আর রেসপন্স সাধারণ টেক্সট হিসেবে লাইন ধরে ধরে দেখতে কেমন।
- একটা URL-এর অংশগুলো, আর Python-এ নিরাপদে URL ভাঙা ও তৈরি করা।
- DNS আর HTTPS/TLS আপনার জন্য কী করে, প্রতিটা কয়েক বাক্যে।
curl -vআর ব্রাউজারের Network ট্যাব দিয়ে HTTP ট্রাফিক দেখা।
ইন্টারনেট, ওয়েব, ক্লায়েন্ট আর সার্ভার
ইন্টারনেট হলো নেটওয়ার্কটা: লাখ লাখ কম্পিউটার এমনভাবে যুক্ত যে IP আর TCP প্রোটোকল ব্যবহার করে যেকোনোটা অন্য যেকোনোটাকে ডেটা পাঠাতে পারে। ওয়েব তার ওপর চলা একটা জিনিস: URL দিয়ে চিহ্নিত ডকুমেন্ট আর ডেটা, যা HTTP দিয়ে আনা হয়। ইমেইল, ভিডিও কল, অনলাইন গেমও ইন্টারনেট ব্যবহার করে, কিন্তু (বেশিরভাগ ক্ষেত্রে) HTTP নয়।
ওয়েবে প্রতিটা কথোপকথনের দুটো দিক থাকে:
- ক্লায়েন্ট একটা রিকোয়েস্ট পাঠিয়ে কথোপকথন শুরু করে। আপনার ব্রাউজার একটা ক্লায়েন্ট।
curl,requestsব্যবহার করা একটা Python স্ক্রিপ্ট, একটা মোবাইল অ্যাপ, আপনার নোটবুকের ভেতরের OpenAI SDK, এরাও ক্লায়েন্ট। - সার্ভার হলো এমন একটা প্রোগ্রাম, যা রিকোয়েস্টের অপেক্ষায় থাকে আর প্রতিটার উত্তরে একটা রেসপন্স দেয়। এটা চলে কোথাও একটা মেশিনে: ডেটা সেন্টারে, ক্লাউডের কোনো কন্টেইনারে, কিংবা আপনার নিজের ল্যাপটপে।
| পরিস্থিতি | ক্লায়েন্ট | সার্ভার |
|---|---|---|
| আপনি একটা ওয়েব পেজ খুললেন | ব্রাউজার | সাইটের ওয়েব সার্ভার |
| আপনার নোটবুক একটা LLM-কে প্রশ্ন করল | Python-এ openai বা anthropic SDK | প্রোভাইডারের API সার্ভার |
| একটা চ্যাট অ্যাপ উত্তর দেখাল | চ্যাট পেজের JavaScript | আপনার ব্যাকএন্ড (FastAPI, এই টিউটোরিয়ালেই পরে) |
| আপনার ব্যাকএন্ড আবহাওয়ার তথ্য চাইল | আপনার ব্যাকএন্ড (এবার ক্লায়েন্টের ভূমিকায়) | একটা ওয়েদার API |
শেষ সারিটা গুরুত্বপূর্ণ: "ক্লায়েন্ট" আর "সার্ভার" হলো একটা কথোপকথনের ভূমিকা, মেশিনের ধরন নয়। একটা সাধারণ AI ব্যাকএন্ড একই সময়ে তার ইউজারদের কাছে সার্ভার, আর LLM প্রোভাইডার, ডেটাবেস ও অন্য API-গুলোর কাছে ক্লায়েন্ট।
অনুশীলনের জন্য একটা সার্ভার
আসল ট্রাফিক দেখতে একটা সার্ভার লাগবে। এই টিউটোরিয়ালে ব্যবহার হবে BitByte Practice API: আপনার নিজের কম্পিউটারে চলা একটা ছোট ওয়েব API, যার ইন্টারনেট লাগে না আর যা প্রতিটা মেশিনে একই উত্তর দেয়, ফলে আপনার আউটপুট এই পাতার সাথে মিলবে। এটা ঠিকঠাকভাবে সেট আপ করবেন ওয়েব API ও REST: ডকুমেন্টেশন পড়া আর curl দিয়ে পরীক্ষা পাতায়। এই পাতার কমান্ডগুলো এখনই চালাতে চাইলে ওই পাতার "Practice API সেট আপ করা" অংশে যান, সেখানকার দুটো ধাপ করুন (ফাইলটা সেভ করা, তারপর দ্বিতীয় একটা টার্মিনালে fastapi run practice_api.py --port 8800 দিয়ে চালু করা), তারপর এখানে ফিরে আসুন। চাইলে শুধু পড়েও যেতে পারেন: নিচের প্রতিটা আউটপুট আসল।
curl দিয়ে ওকে কিছু জিজ্ঞেস করুন। curl হলো কমান্ড লাইনের একটা HTTP ক্লায়েন্ট, যা macOS, Linux আর Windows 10/11-এ আগে থেকেই থাকে:
curl -s http://localhost:8800/{"name":"BitByte Practice API","version":"1.0.0","docs":"/docs"}-s (silent) curl-এর প্রগ্রেস মিটার লুকিয়ে রাখে; নইলে curl-এর আউটপুট কোনো পাইপ বা ফাইলে গেলে মিটারটা দেখা যায়। এই টিউটোরিয়ালে প্রায় সবখানে এটা ব্যবহার হবে। localhost মানে "এই কম্পিউটারটাই", আর 8800 হলো সেই পোর্ট, যেখানে সার্ভার শুনছে। আপনার টার্মিনাল (ক্লায়েন্ট) একটা রিকোয়েস্ট পাঠিয়েছে, সার্ভার উত্তর দিয়েছে, আর curl উত্তরের বডি প্রিন্ট করেছে। বডিটা JSON, যে ফরম্যাট প্রায় সব API ব্যবহার করে।
HTTP দেখতে কেমন
HTTP একটা টেক্সট প্রোটোকল: রিকোয়েস্ট আর রেসপন্স হলো নির্দিষ্ট আকারের কিছু টেক্সট লাইন। ছবিতে আকারটা এরকম:
REQUEST (client → server) RESPONSE (server → client)
GET /v1/courses/2 HTTP/1.1 ← request line HTTP/1.1 200 OK ← status line
Host: localhost:8800 ← headers content-type: application/json ← headers
User-Agent: curl/8.5.0 content-length: 75
Accept: */*
← blank line ← blank line
(body: empty for GET) ← body {"id":2,"title":"Python for AI",...} ← body- রিকোয়েস্ট লাইন বলে ক্লায়েন্ট কী চায়: একটা মেথড (
GET= "আমাকে দাও"), একটা পাথ (/v1/courses/2) আর প্রোটোকলের ভার্সন। - স্ট্যাটাস লাইন বলে কাজটা কেমন হলো: তিন অঙ্কের একটা স্ট্যাটাস কোড (
200) আর ছোট একটা কারণ (OK)। - হেডার হলো
Name: valueআকারের লাইন, যাতে বাড়তি তথ্য থাকে: কোন হোস্ট, বডি কোন ফরম্যাটে, কত বড়, কে জিজ্ঞেস করছে, আর (পরে) আপনার API কি। - একটা ফাঁকা লাইন হেডার শেষ করে। এর পরের সবকিছু হলো বডি।
প্রমাণ: হাতে লেখা HTTP
HTTP-তে কথা বলতে কোনো বিশেষ লাইব্রেরি লাগে না। Python-এর socket মডিউল একটা সাধারণ নেটওয়ার্ক কানেকশন খোলে; রিকোয়েস্টটা আমরা নিজেরাই লিখব, আর যা ফেরত আসে হুবহু প্রিন্ট করব। (date নামের একটা হেডার প্রতি সেকেন্ডে বদলায়, তাই প্রিন্ট থেকে ওটা বাদ রাখছি।)
import socket
request = (
"GET /v1/courses/2 HTTP/1.1\r\n"
"Host: localhost:8800\r\n"
"Connection: close\r\n"
"\r\n"
)
with socket.create_connection(("localhost", 8800)) as sock:
sock.sendall(request.encode("ascii"))
raw = b""
while chunk := sock.recv(4096):
raw += chunk
for line in raw.decode("utf-8").split("\r\n"):
if not line.lower().startswith("date:"):
print(line)HTTP/1.1 200 OK
server: uvicorn
content-length: 75
content-type: application/json
connection: close
{"id":2,"title":"Python for AI","level":"beginner","hours":12,"rating":4.8}যা খেয়াল করবেন:
- প্রতিটা লাইন শেষ হয়
\r\n(carriage return + line feed) দিয়ে, আর একটা খালি লাইন হেডার শেষ করে। পুরো "ফরম্যাট" এটুকুই। - HTTP/1.1-এ
Hostবাধ্যতামূলক, কারণ একটা সার্ভার মেশিনে প্রায়ই অনেকগুলো সাইট থাকে। Connection: closeদিয়ে আমরা সার্ভারকে বলেছি উত্তর দিয়ে কানেকশন বন্ধ করতে, তাই আমাদের লুপ বোঝে কখন পড়া থামাতে হবে। সাধারণ ক্লায়েন্টরা কানেকশন খোলা রেখে আবার ব্যবহার করে (পরেrequests.Session-এ এটা ব্যবহার করবেন)।content-length: 75হলো বডির আকার, বাইটে। JSON-এর অক্ষরগুলো গুনে দেখুন: ৭৫টা।- হেডারের নাম কেস-ইনসেনসিটিভ: সার্ভার লিখেছে
content-type, ডকুমেন্টেশনে প্রায়ই লেখা থাকেContent-Type। দুটোর মানে একই।
আসল কাজে আপনাকে কখনো হাতে HTTP লিখতে হবে না; curl, requests আর SDK-গুলো সেটা করে দেয়। কিন্তু এখন আপনি জানেন এতে কোনো জাদু নেই: একটা LLM API কল হলো ঠিক এটাই, সাথে একটা POST, একটা JSON বডি আর একটা Authorization হেডার।
HTTP-এর ভার্সন
ওপরের টেক্সটটা HTTP/1.1। নতুন ভার্সনগুলো বদলায় বাইটগুলো কীভাবে যাবে, তাদের মানে নয়:
| ভার্সন | নেটওয়ার্কে কীভাবে যায় | যা জানা দরকার |
|---|---|---|
| HTTP/1.1 | TCP-র ওপর সাধারণ টেক্সট | এখনো সবখানে আছে; curl -v-এ সহজে পড়া যায়। |
| HTTP/2 | বাইনারি ফ্রেম, অনেক রিকোয়েস্ট একটা কানেকশন ভাগ করে নেয় | বেশিরভাগ বড় API আর ব্রাউজার এটা ব্যবহার করে। মেথড, হেডার, স্ট্যাটাস কোড একই। |
| HTTP/3 | QUIC (UDP)-এর ওপর বাইনারি ফ্রেম | দুর্বল মোবাইল নেটওয়ার্কে দ্রুততর। মানে আবারও একই। |
মেথড, পাথ, হেডার, স্ট্যাটাস কোড আর বডি তিনটাতেই একইভাবে কাজ করে; এ কারণেই এই টিউটোরিয়াল HTTP/1.1 দিয়ে শেখাতে পারে।
আরেকটা বৈশিষ্ট্য: HTTP স্টেটলেস (stateless)। প্রতিটা রিকোয়েস্ট স্বতন্ত্র; আপনি নিজেকে চেনানোর মতো কিছু (একটা টোকেন, একটা কুকি) না পাঠালে সার্ভার আপনার আগের রিকোয়েস্ট মনে রাখে না। এ কারণেই প্রতিটা LLM API কলে API কি আবার পাঠাতে হয়, আর একটা চ্যাট API-তে প্রতিটা রিকোয়েস্টে পুরো কথোপকথনের ইতিহাস পাঠাতে হয়: দুই কলের মাঝে সার্ভার কিছুই মনে রাখে না।
URL আর তার অংশগুলো
URL (Uniform Resource Locator) বলে কোনো কিছু কোথায় আছে আর কীভাবে পাওয়া যাবে। এটাকে ভেঙে দেখুন:
https://api.example.com:8443/v1/courses?level=beginner&page=2#reviews
└─┬─┘ └──────┬──────┘ └┬─┘└────┬────┘ └─────────┬─────────┘ └──┬──┘
scheme host port path query fragment| অংশ | উদাহরণ | মানে |
|---|---|---|
| স্কিম (scheme) | https | কোন প্রোটোকল: http নাকি https। |
| হোস্ট (host) | api.example.com | কোন মেশিন (একটা ডোমেইন নাম বা IP অ্যাড্রেস)। |
| পোর্ট (port) | 8443 | ওই মেশিনের কোন প্রোগ্রাম। সাধারণত লেখা হয় না: http মানে 80, https মানে 443। |
| পাথ (path) | /v1/courses | সার্ভারের কোন রিসোর্স। |
| কোয়েরি স্ট্রিং (query string) | level=beginner&page=2 | ?-এর পরে বাড়তি অপশন, key=value জোড়া হিসেবে, & দিয়ে জোড়া। |
| ফ্র্যাগমেন্ট (fragment) | reviews | পেজের ভেতরের একটা জায়গা, #-এর পরে। শুধু ব্রাউজার ব্যবহার করে; সার্ভারে কখনো পাঠানো হয় না। |
Python-এর স্ট্যান্ডার্ড লাইব্রেরি আপনার হয়ে URL ভেঙে দিতে পারে, যা স্ট্রিং কেটে কেটে করার চেয়ে নিরাপদ:
from urllib.parse import urlsplit, parse_qs
url = "https://api.example.com:8443/v1/courses?level=beginner&page=2#reviews"
parts = urlsplit(url)
print("scheme: ", parts.scheme)
print("host: ", parts.hostname)
print("port: ", parts.port)
print("path: ", parts.path)
print("query: ", parts.query)
print("fragment:", parts.fragment)
print(parse_qs(parts.query))scheme: https
host: api.example.com
port: 8443
path: /v1/courses
query: level=beginner&page=2
fragment: reviews
{'level': ['beginner'], 'page': ['2']}parse_qs লিস্ট দেয়, কারণ একটা কি একাধিকবার আসতে পারে (?tag=ai&tag=python)। আরও খেয়াল করুন, কোয়েরির মান সবসময় টেক্সট: '2', 2 নয়। একে সংখ্যা ধরবে কি না, তা ঠিক করে সার্ভার।
বিশেষ অক্ষর: পার্সেন্ট-এনকোডিং
URL-এ শুধু নির্দিষ্ট কিছু অক্ষর থাকতে পারে। কোনো মানের ভেতরে স্পেস, ?, &, #, কোটেশন আর ইংরেজি নয় এমন অক্ষর থাকলে সেগুলোকে পার্সেন্ট-এনকোড (percent-encode) করতে হয়: প্রতিটা বাইট হয়ে যায় % আর দুটো হেক্স অঙ্ক। এমন মান কখনো হাতে জোড়া দিয়ে URL বানাবেন না; লাইব্রেরিকে এনকোড করতে দিন:
from urllib.parse import urlencode, quote
print(urlencode({"q": "what is RAG?", "city": "Cox's Bazar"}))
print(quote("ঢাকা"))q=what+is+RAG%3F&city=Cox%27s+Bazar
%E0%A6%A2%E0%A6%BE%E0%A6%95%E0%A6%BEপ্রশ্নের ভেতরের ? হয়ে গেছে %3F; নইলে সার্ভার ভাবত সেখান থেকে নতুন একটা কোয়েরি স্ট্রিং শুরু হচ্ছে। কোয়েরি স্ট্রিংয়ে স্পেস হলো + (অথবা %20)। বাংলা শব্দ "ঢাকা" UTF-8-এ ১২ বাইট, তাই এটা হয়ে যায় ১২টা %XX গ্রুপ। পরের পাতাগুলোতে দেখবেন, requests আর curl --data-urlencode এটা নিজেরাই করে দেয়।
DNS: নাম থেকে অ্যাড্রেস
কম্পিউটার কানেক্ট করে IP অ্যাড্রেসে, যেমন 127.0.0.1 (IPv4) বা ::1 (IPv6), নামে নয়। DNS (Domain Name System) হলো ইন্টারনেটের ফোনবুক: api.openai.com-এ কানেক্ট করার আগে আপনার কম্পিউটার একটা DNS রিজলভারকে (সাধারণত আপনার ISP, রাউটার বা কোনো পাবলিক সার্ভিস যেটা চালায়) জিজ্ঞেস করে "এই নামের অ্যাড্রেস কী?", উত্তরটা কিছুক্ষণ ক্যাশে রাখে, তারপর সেই অ্যাড্রেসে কানেক্ট করে। localhost বিশেষ: এর উত্তর আসে আপনার নিজের মেশিন থেকে, আর এর মানে সবসময় "এই কম্পিউটারটাই"।
import socket
print(socket.gethostbyname("localhost"))127.0.0.1DNS ব্যর্থ হলে কোনো HTTP শুরু হওয়ার আগেই Name or service not known বা getaddrinfo failed (Windows)-এর মতো এরর দেখবেন। সাধারণত এর মানে হোস্টের নামে টাইপো, ইন্টারনেট নেই, অথবা অফিসের নেটওয়ার্ক নামটা ব্লক করছে; API-র নিজের কোনো সমস্যা নয়।
HTTPS আর TLS
সাধারণ HTTP যায় পড়া যায় এমন টেক্সট হিসেবে, তাই পথে থাকা যে কেউ (ক্যাফের Wi-Fi, একটা হ্যাক হওয়া রাউটার) সেটা পড়তে বা বদলাতে পারে, আপনার API কি-সহ। HTTPS হলো একটা TLS কানেকশনের ভেতরে পাঠানো HTTP (আজকাল TLS 1.3)। TLS আপনাকে তিনটা জিনিস দেয়:
| নিশ্চয়তা | মানে | AI অ্যাপের জন্য কেন জরুরি |
|---|---|---|
| এনক্রিপশন | শুধু দুই প্রান্তই ট্রাফিক পড়তে পারে। | প্রম্পট, ইউজারের ডেটা আর API কি পথে গোপন থাকে। |
| ইন্টেগ্রিটি | পথে কোনো পরিবর্তন হলে ধরা পড়ে। | কেউ চুপচাপ মডেলের উত্তর বা আপনার রিকোয়েস্ট বদলে দিতে পারে না। |
| সার্ভারের পরিচয় | সার্ভার একটা সার্টিফিকেট দিয়ে প্রমাণ করে সে কে; সার্টিফিকেটটা এমন এক সার্টিফিকেট অথরিটির স্বাক্ষরিত, যাকে আপনার সিস্টেম বিশ্বাস করে। | আপনি সত্যিই api.anthropic.com-এর সাথে কথা বলছেন, কোনো ছদ্মবেশীর সাথে নয়। |
ভেতরের HTTP মেসেজগুলো হুবহু আগের মতোই; TLS শুধু সেগুলোকে মুড়ে দেয়। এখান থেকে দুটো কাজের নিয়ম:
- আপনার কম্পিউটার ছেড়ে বাইরে যায় এমন সবকিছুতে
https://ব্যবহার করুন। প্রতিটা আসল AI প্রোভাইডার শুধু HTTPS নেয়। সাধারণhttp://চলে শুধুlocalhost-এর জন্য, যেমন Practice API, কারণ সেই ট্রাফিক আপনার মেশিন থেকে বেরোয়ই না। - "এররটা চলে যাক" ভেবে কখনো সার্টিফিকেট চেক বন্ধ করবেন না (
curl -k,requests-এverify=False)। সার্টিফিকেট এররের মানে হয় সেটআপে ভুল (ঠিক করুন), নয়তো মাঝখানে কেউ আছে (থামুন)।
curl -v https://… চালালে * দিয়ে শুরু হওয়া লাইনগুলো TLS হ্যান্ডশেক দেখায়: TLS ভার্সন, সার্ভার সার্টিফিকেটের subject (যে নামে ইস্যু করা), মেয়াদ শেষের তারিখ আর ভেরিফিকেশন সফল হলো কি না। কোনো স্ট্যাটাস কোড আসার আগেই HTTPS কল ব্যর্থ হলে এই লাইনগুলো পড়ুন।
curl -v দিয়ে ট্রাফিক দেখা
-v (verbose) দিলে curl পুরো কথোপকথন দেখায়: > দিয়ে শুরু হওয়া লাইন হলো curl যা পাঠিয়েছে, < দিয়ে শুরু হওয়া লাইন হলো সার্ভার যা ফেরত পাঠিয়েছে, আর * দিয়ে শুরু হওয়া লাইন curl-এর নিজের নোট (নাম খোঁজা, কানেক্ট করা, TLS)। curl এগুলো লেখে stderr-এ, তাই 2>&1 এগুলোকে বডির সাথে জুড়ে দেয়। এই পাতার আউটপুট স্থির রাখতে আমরা শুধু > আর < লাইনগুলো রাখছি আর বদলাতে থাকা date হেডার বাদ দিচ্ছি (tr -d '\r' অদৃশ্য carriage return-গুলো সরায়):
curl -sv http://localhost:8800/v1/courses/2 2>&1 | grep -E '^[<>]' | grep -v '^< date' | tr -d '\r'> GET /v1/courses/2 HTTP/1.1
> Host: localhost:8800
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< server: uvicorn
< content-length: 75
< content-type: application/json
<এটা সেই একই রিকোয়েস্ট আর রেসপন্স, যা ওপরে আপনি হাতে লিখেছিলেন। curl নিজে থেকে দুটো হেডার যোগ করেছে: User-Agent (কে জিজ্ঞেস করছে; আপনার curl-এর ভার্সন নম্বর আলাদা হতে পারে) আর Accept: */* ("যেকোনো ফরম্যাট চলবে")। ফিল্টার ছাড়া নিজে সাধারণ curl -v চালালে * লাইনগুলো আর বডিও দেখবেন।
শুধু রেসপন্সের স্ট্যাটাস লাইন আর হেডার বডির সাথে একসাথে চাইলে -i (include) আরও ছোট:
curl -si "http://localhost:8800/v1/weather?city=Dhaka" | grep -v '^date' | tr -d '\r'HTTP/1.1 200 OK
server: uvicorn
content-length: 64
content-type: application/json
{"city":"Dhaka","temp_c":31.5,"humidity":78,"condition":"Humid"}URL-টা ডাবল কোটেশনের ভেতরে। এটা সাজসজ্জা নয়: bash-এ ? একটা ওয়াইল্ডকার্ড আর & কমান্ডকে ব্যাকগ্রাউন্ডে চালায়, তাই কোয়েরি স্ট্রিংসহ কোটেশন ছাড়া URL বিভ্রান্তিকরভাবে ভেঙে যেতে পারে।
সার্ভার চেনে না এমন একটা শহরের কথা জিজ্ঞেস করুন; বডি পড়ার আগেই স্ট্যাটাস লাইন আপনাকে বলে দেবে:
curl -si "http://localhost:8800/v1/weather?city=Comilla" | grep -v '^date' | tr -d '\r'HTTP/1.1 404 Not Found
server: uvicorn
content-length: 42
content-type: application/json
{"detail":"No weather data for 'Comilla'"}404 মানে "পাওয়া যায়নি"। পরের পাতায় স্ট্যাটাস কোড ঠিকঠাক দেখা হবে।
ব্রাউজারে ট্রাফিক দেখা: DevTools
প্রতিটা ব্রাউজারেই একই ধরনের টুল বিল্ট-ইন থাকে। http://localhost:8800/v1/courses (বা যেকোনো ওয়েবসাইট) খুলুন, তারপর F12 চাপুন (macOS-এ Cmd+Option+I) আর Network ট্যাব বেছে নিন। পেজটা রিলোড করুন। প্রতিটা সারি একটা HTTP রিকোয়েস্ট:
| কলাম / প্যানেল | যা দেখায় |
|---|---|
| Name, Method, Status | পাথ, GET/POST…, আর স্ট্যাটাস কোড (4xx/5xx লাল রঙে)। |
| Type, Size, Time | কনটেন্ট টাইপ, কত বাইট, আর কত সময় লেগেছে। |
| Headers (একটা সারিতে ক্লিক করুন) | রিকোয়েস্ট আর রেসপন্সের সব হেডার, curl -v-এর মতো। |
| Payload / Preview / Response | রিকোয়েস্টের বডি, আর রেসপন্সের বডি (সাজানো বা কাঁচা)। |
| Timing | DNS খোঁজা, কানেক্ট করা, TLS, সার্ভারের অপেক্ষা, ডাউনলোড। |
রোজ কাজে লাগে এমন দুটো কৌশল: Preserve log টিক দিন, যাতে পেজ বদলালেও রিকোয়েস্টগুলো থাকে; আর একটা রিকোয়েস্টে রাইট-ক্লিক করে Copy → Copy as cURL দিন, তাহলে এমন একটা curl কমান্ড পাবেন যা টার্মিনালে হুবহু ওই রিকোয়েস্টটাই আবার পাঠায়। Network ট্যাব খোলা রেখে একটা চ্যাট অ্যাপ (ChatGPT, Claude, পরে আপনার নিজেরটা) খুলে একটা মেসেজ পাঠান: যে POST-টায় আপনার প্রম্পট যাচ্ছে সেটা দেখবেন, আর দেখবেন উত্তরটা স্ট্রিম হিসেবে একটু একটু করে আসছে (EventStream ভিউ)। এটাও সেই একই HTTP, যা আপনি এখানে শিখছেন।
"Copy as cURL"-এ আপনার কুকি আর টোকেনও থাকে। কপি করা কমান্ডটাকে পাসওয়ার্ডের মতো দেখুন: কোনো চ্যাট, ইস্যু বা কমিটে পেস্ট করবেন না।
কোথায় কাজে লাগবে
- LLM কল ডিবাগ করা। একটা SDK এরর দিলে কাজের তথ্যগুলো HTTP-রই তথ্য: কোন URL, কোন স্ট্যাটাস কোড, কোন হেডার (rate-limit, retry-after) আর কোন বডি। এ কারণেই অনেক SDK কাঁচা রিকোয়েস্টগুলো লগ করতে পারে।
- এক ব্যাকএন্ড থেকে আরেক ব্যাকএন্ডে কল। আপনার FastAPI সার্ভিস তার ইউজারদের কাছে সার্ভার, আর মডেল প্রোভাইডার, ভেক্টর ডেটাবেস ও অন্য API-র কাছে ক্লায়েন্ট হবে। প্রতিটা ধাপ ওপরের মতোই একটা রিকোয়েস্ট/রেসপন্স।
- ফ্রন্টএন্ড আর ব্যাকএন্ডের বাগ। "ব্রাউজার আদৌ রিকোয়েস্টটা পাঠিয়েছিল কি না, আর সার্ভার কী বলেছে?", এই প্রশ্নের উত্তর Network ট্যাব কয়েক সেকেন্ডে দেয়, কেউ কোড খোলার আগেই।
- সিকিউরিটি রিভিউ। "কিছু কি সাধারণ
http://-তে যাচ্ছে? কি-গুলো কি হেডারের বদলে URL-এ (যা লগে জমা হয়) যাচ্ছে?", এগুলো URL আর হেডার নিয়েই প্রশ্ন।
সাধারণ ভুল
- টার্মিনালে কোটেশন ছাড়া URL।
curl http://localhost:8800/v1/courses?level=beginner&page=2চালালেcurl …?level=beginnerব্যাকগ্রাউন্ডে চলে, তারপর আলাদা একটা "কমান্ড"page=2চলে।?বা&থাকা URL সবসময় কোটেশনে রাখুন। - স্ট্রিং জোড়া দিয়ে URL বানানো। প্রশ্নে
?,&,#বা কোনো বাংলা অক্ষর এলেই"...?q=" + questionভেঙে যায়।urlencode,requests-এরparams=, বাcurl --data-urlencodeব্যবহার করুন। - ভাবা যে ফ্র্যাগমেন্ট সার্ভারে পৌঁছায়।
#-এর পরের সবকিছু ব্রাউজারেই থেকে যায়। সার্ভারের কোনো মান দরকার হলে সেটা পাথ বা কোয়েরিতে দিন। - আসল সার্ভিসের জন্য
http://ব্যবহার বা সার্টিফিকেট চেক বন্ধ করা। তখন আপনার API কি পড়া যায় এমন অবস্থায় যায়, অথবা আপনি কোনো ছদ্মবেশীর সাথে কথা বলছেন হয়তো।https://ব্যবহার করুন, আর সার্টিফিকেট এরর লুকিয়ে না রেখে ঠিক করুন। - "কানেক্ট করা যাচ্ছে না" আর HTTP এররকে গুলিয়ে ফেলা। "Connection refused" বা "Name or service not known" মানে কোনো HTTP কথোপকথনই হয়নি (সার্ভার চলছে না, ভুল পোর্ট, ভুল হোস্ট)। 404 বা 500 মানে সার্ভার উত্তর দিয়েছে। সমাধান আলাদা।
নিজে চেষ্টা করুন
- সহজ: curl দিয়ে Practice API থেকে
/healthআনুন, তারপর সিলেটের আবহাওয়া। সিলেটের রিকোয়েস্টের স্ট্যাটাস লাইন আর হেডারও দেখান। - মাঝারি: Python-এ
urlsplitদিয়েhttp://localhost:8800/v1/courses?level=advanced&per_page=2-কে অংশে ভাগ করুন, আর হোস্ট, পোর্ট, পাথ ও কোয়েরি (ডিকশনারি হিসেবে) প্রিন্ট করুন। URL যদিhttps://api.example.com/v1/coursesহতো, কোন পোর্ট ব্যবহার হতো? - কঠিন: raw-socket প্রোগ্রামটা বদলে দিন, যাতে এটা
/v1/courses/99চায় আর শুধু স্ট্যাটাস লাইন ও বডি প্রিন্ট করে। তারপর ব্যাখ্যা করুন, প্রোগ্রামটা কীভাবে বোঝে বডি কোথা থেকে শুরু।
উত্তর
# 1
curl -s http://localhost:8800/health
echo
curl -si "http://localhost:8800/v1/weather?city=Sylhet" | grep -v '^date' | tr -d '\r'{"status":"ok"}
HTTP/1.1 200 OK
server: uvicorn
content-length: 64
content-type: application/json
{"city":"Sylhet","temp_c":27.4,"humidity":85,"condition":"Rain"}echo একটা লাইন ব্রেক প্রিন্ট করে, কারণ প্রথম রেসপন্সের বডি লাইন ব্রেক দিয়ে শেষ হয় না।
# 2
from urllib.parse import urlsplit, parse_qs
parts = urlsplit("http://localhost:8800/v1/courses?level=advanced&per_page=2")
print(parts.hostname, parts.port, parts.path)
print(parse_qs(parts.query))
print(urlsplit("https://api.example.com/v1/courses").port)localhost 8800 /v1/courses
{'level': ['advanced'], 'per_page': ['2']}
NoneURL-এ পোর্ট লেখা না থাকলে port হয় None। তখন ক্লায়েন্ট স্কিমের ডিফল্ট পোর্ট ব্যবহার করে: https-এর জন্য 443।
# 3
import socket
request = (
"GET /v1/courses/99 HTTP/1.1\r\n"
"Host: localhost:8800\r\n"
"Connection: close\r\n"
"\r\n"
)
with socket.create_connection(("localhost", 8800)) as sock:
sock.sendall(request.encode("ascii"))
raw = b""
while chunk := sock.recv(4096):
raw += chunk
head, body = raw.decode("utf-8").split("\r\n\r\n", 1)
print(head.split("\r\n")[0])
print(body)HTTP/1.1 404 Not Found
{"detail":"Course 99 not found"}হেডার শেষ হয় প্রথম খালি লাইনে, যা কাঁচা বাইটে হলো \r\n\r\n (শেষ হেডার লাইনের সমাপ্তি আর একটা খালি লাইন)। ওই চিহ্নে একবার ভাগ করলে পাওয়া যায় হেড (স্ট্যাটাস লাইন + হেডার) আর বডি। আসল ক্লায়েন্টরা বডিতে কত বাইট আসবে জানতে content-length-ও পড়ে।
সারসংক্ষেপ
- ইন্টারনেট কম্পিউটারগুলোকে যুক্ত করে; ওয়েব হলো তার ওপর URL আর HTTP। ক্লায়েন্ট রিকোয়েস্ট পাঠায়, সার্ভার রেসপন্স পাঠায়। একই প্রোগ্রাম দুটোই হতে পারে।
- একটা HTTP মেসেজ হলো একটা শুরুর লাইন (রিকোয়েস্ট লাইন বা স্ট্যাটাস লাইন), হেডার, একটা খালি লাইন আর ঐচ্ছিক বডি। HTTP/2 আর HTTP/3 পরিবহন বদলায়, মানে নয়। HTTP স্টেটলেস।
- URL-এ থাকে স্কিম, হোস্ট, পোর্ট, পাথ, কোয়েরি স্ট্রিং আর ফ্র্যাগমেন্ট। URL ভাঙা আর এনকোড করার কাজ লাইব্রেরিকে দিন; ইউজারের টেক্সট কখনো হাতে জোড়া দেবেন না।
- DNS নামকে IP অ্যাড্রেসে বদলায়। HTTPS = TLS-এর ভেতরে HTTP: এনক্রিপ্টেড, বদলানো যায় না, আর সার্ভার তার পরিচয় প্রমাণ করে।
localhost-এর বাইরের সবকিছুতে এটা ব্যবহার করুন। curl -v,curl -iআর ব্রাউজারের Network ট্যাব দিয়ে আসল রিকোয়েস্ট আর রেসপন্স দেখা যায়। API কল ব্যর্থ হলে আগে এখানেই দেখুন।
এরপর: HTTP মেথড, স্ট্যাটাস কোড, হেডার ও JSON। আপনি GET আর 200/404 দেখেছেন; এরপর আসছে বাকি মেথডগুলো, স্ট্যাটাস কোডের পুরো পরিবার, API-র কাছে গুরুত্বপূর্ণ হেডারগুলো, আর JSON বডি।