রাউটিং ও রিকোয়েস্ট লাইফসাইকেল
এই পাঠে যা শিখবেন
- রিকোয়েস্ট লাইফসাইকেলের পাঁচটি ধাপ এবং প্রতিটির দায়িত্ব
- রাউট ম্যাচ না হলে কীভাবে বাকি ধাপগুলো (মিডলওয়্যার, হ্যান্ডলার) এড়িয়ে যাওয়া হয়
- একটি
LifecycleRouterক্লাস — যা প্রতিটি ধাপ real Python ফাংশন কলের মাধ্যমে সম্পন্ন করে - মিডলওয়্যার ও হ্যান্ডলারের মধ্যে দায়িত্বের পার্থক্য — পরবর্তী পাঠ (L19) এই মিডলওয়্যার অংশটিকেই পূর্ণাঙ্গভাবে সম্প্রসারিত করবে
১ · রিকোয়েস্ট লাইফসাইকেলের পাঁচটি ধাপ
যখন একটি HTTP রিকোয়েস্ট সার্ভারে পৌঁছায়, একটি ব্যাক-এন্ড ফ্রেমওয়ার্ক সেটিকে সরাসরি হ্যান্ডলারে পাঠিয়ে দেয় না — বরং নির্দিষ্ট একটি পাইপলাইনের মধ্য দিয়ে পাস করায়। এই পাইপলাইনের প্রতিটি ধাপের একটি নির্দিষ্ট, সংকীর্ণ দায়িত্ব থাকে।
raw রিকোয়েস্ট লাইন ও হেডার থেকে একটি স্ট্রাকচার্ড request অবজেক্ট/dict তৈরি হয় — method, path, headers, body।
method+path জোড়াটি রেজিস্টার করা রাউটগুলোর সাথে মেলানো হয়। না মিললে বাকি ধাপ বাদ দিয়ে সরাসরি 404।
লগিং, অথ-চেক ইত্যাদি ক্রস-কাটিং কনসার্ন হ্যান্ডলারের আগে ক্রমানুসারে চলে (পূর্ণাঙ্গ প্যাটার্ন L19)।
নির্দিষ্ট রুটের জন্য লেখা আসল ফাংশন -- ডেটা প্রসেস করে একটি ফলাফল রিটার্ন করে।
২ · LifecycleRouter — প্রতিটি ধাপ সত্যিকারের ফাংশন কল হিসেবে
নিচের কোডে L01-এর Router ক্লাসটি সম্প্রসারণ করে একটি LifecycleRouter তৈরি করা
হয়েছে যার process() মেথড উপরের পাঁচটি ধাপই ধারাবাহিকভাবে চালায় এবং প্রতিটি ধাপ শুরু হওয়ার সময়
প্রিন্ট করে দেখায় — একটি সফল রিকোয়েস্ট ও একটি না-মেলা রুটের রিকোয়েস্ট, দুটোই ট্রেস করা হয়েছে।
# ধাপ ১ -- পার্সিং: raw রিকোয়েস্ট লাইন থেকে একটি স্ট্রাকচার্ড request dict
def parse_request(raw_line, headers, body):
method, path, protocol = raw_line.split(" ")
return {"method": method, "path": path, "headers": headers, "body": body, "protocol": protocol}
class LifecycleRouter:
def __init__(self):
self.routes = {}
self.middlewares = []
def use(self, middleware_fn):
self.middlewares.append(middleware_fn)
def route(self, method, path):
def decorator(handler_fn):
self.routes[(method, path)] = handler_fn
return handler_fn
return decorator
def process(self, raw_line, headers, body):
# ধাপ ১ -- পার্সিং
print("ধাপ ১ -- পার্সিং")
request = parse_request(raw_line, headers, body)
print(f" parsed: {request['method']} {request['path']}")
# ধাপ ২ -- রাউট ম্যাচিং
print("ধাপ ২ -- রাউট ম্যাচিং")
handler = self.routes.get((request["method"], request["path"]))
if handler is None:
print(" কোনো রাউট মেলেনি -- বাকি ধাপ এড়িয়ে সরাসরি 404")
return {"status": 404, "headers": {}, "body": "Not Found"}
print(f" মিলেছে: {handler.__name__}()")
# ধাপ ৩ -- মিডলওয়্যার চেইন (ক্রমানুসারে)
print("ধাপ ৩ -- মিডলওয়্যার চেইন")
for mw in self.middlewares:
print(f" চলছে: {mw.__name__}")
mw(request)
# ধাপ ৪ -- হ্যান্ডলার এক্সিকিউশন
print("ধাপ ৪ -- হ্যান্ডলার এক্সিকিউশন")
result = handler(request)
print(f" হ্যান্ডলার রিটার্ন করেছে: {result}")
# ধাপ ৫ -- রেসপন্স ফরম্যাটিং
print("ধাপ ৫ -- রেসপন্স ফরম্যাটিং")
response = {"status": 200, "headers": {"Content-Type": "application/json"}, "body": result}
print(f" status={response['status']}")
return response
def log_middleware(request):
print(f" [log] {request['method']} {request['path']} রিকোয়েস্ট এসেছে")
app = LifecycleRouter()
app.use(log_middleware)
@app.route("GET", "/users/42")
def get_user(request):
return {"id": 42, "name": "রবিন"}
print("=== ট্রেস ১: মেলা রুট ===")
response1 = app.process("GET /users/42 HTTP/1.1", {"Accept": "application/json"}, None)
print("চূড়ান্ত রেসপন্স:", response1)
print("\n=== ট্রেস ২: না-মেলা রুট ===")
response2 = app.process("GET /users/999 HTTP/1.1", {"Accept": "application/json"}, None)
print("চূড়ান্ত রেসপন্স:", response2)
ধাপ ৩, ধাপ ৪, ও ধাপ ৫-এর প্রিন্ট লাইনগুলো একেবারেই
দেখা যায় না — কারণ process() মেথডে রাউট ম্যাচিং ব্যর্থ হলে return স্টেটমেন্ট
সাথে সাথে ফাংশন থেকে বেরিয়ে যায়। এটিই লাইফসাইকেলের একটি গুরুত্বপূর্ণ বৈশিষ্ট্য — প্রতিটি ধাপ পরবর্তী ধাপে
যাওয়ার একটি "গেটকিপার"।
৩ · কেন প্রতিটি ধাপ আলাদা রাখা হয়
এই পাঁচটি ধাপকে আলাদা রাখার সুবিধা হলো প্রতিটির দায়িত্ব একক ও স্পষ্ট (single responsibility) —
parse_request() শুধু raw ডেটাকে গঠন দেয়, রাউট ম্যাচিং শুধু কোন হ্যান্ডলার চলবে তা ঠিক করে,
মিডলওয়্যার ক্রস-কাটিং কনসার্ন (লগিং, অথ) সামলায়, হ্যান্ডলার শুধু বিজনেস লজিক লেখে, আর রেসপন্স ফরম্যাটিং
সব হ্যান্ডলারের জন্য একই কনসিস্টেন্ট আউটপুট আকার নিশ্চিত করে। প্রতিটি Express, Django, Rails, Laravel বা
Spring-এর মতো ফ্রেমওয়ার্কই এই একই পাঁচটি ধাপ অনুসরণ করে, শুধু সিনট্যাক্স ও ধাপগুলোর নাম আলাদা হতে পারে।
একটি HTTP রিকোয়েস্ট সরাসরি হ্যান্ডলারে পৌঁছায় না — এটি পার্সিং, রাউট ম্যাচিং, মিডলওয়্যার চেইন, হ্যান্ডলার এক্সিকিউশন, ও রেসপন্স ফরম্যাটিং — এই পাঁচটি ধাপের একটি নির্দিষ্ট পাইপলাইন পার হয়। যেকোনো ধাপ (বিশেষত রাউট ম্যাচিং বা মিডলওয়্যার) পরবর্তী ধাপগুলো এড়িয়ে সরাসরি একটি রেসপন্স তৈরি করে দিতে পারে — এই "শর্ট-সার্কিট" আচরণটি পরের পাঠে (L19) মিডলওয়্যার প্যাটার্নে আরও গভীরভাবে দেখা যাবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ রাউট ম্যাচিং ধাপটি মিডলওয়্যার চেইনের আগে কেন রাখা হয়, পরে নয়?
যদি কোনো পাথের জন্য কোনো হ্যান্ডলারই নিবন্ধিত না থাকে, তাহলে মিডলওয়্যার চালানোর কোনো অর্থ নেই — লগিং বা অথ-চেক করেও শেষে 404-ই ফেরত যাবে। রাউট ম্যাচিং আগে করে ফ্রেমওয়ার্ক অপ্রয়োজনীয় কাজ (মিডলওয়্যার চালানো) এড়িয়ে যায় এবং দ্রুত 404 রিটার্ন করে। এটি একটি পারফরম্যান্স ও স্পষ্টতা — দুটোরই সিদ্ধান্ত।
প্র ০২
উপরের কোডে parse_request() কেন LifecycleRouter ক্লাসের ভেতরের মেথড না
হয়ে আলাদা একটি ফাংশন?
পার্সিং লজিকটি রাউটিং-নির্দিষ্ট কিছু নয় -- এটি শুধু raw টেক্সট থেকে একটি স্ট্রাকচার্ড dict তৈরি করে, যা
Router-এর কোনো state (যেমন self.routes) স্পর্শ করে না। এটিকে আলাদা রাখলে
ফাংশনটি স্বতন্ত্রভাবে টেস্ট করা যায় এবং প্রয়োজনে অন্য কোনো কনটেক্সটেও পুনরায় ব্যবহার করা যায় -- এটি
"একক দায়িত্ব" নীতিরই একটি ব্যবহারিক উদাহরণ।
প্র ০৩
যদি log_middleware রাউট ম্যাচিং-এর আগে রাখা হতো, তাহলে ট্রেস ২-এ কী পার্থক্য দেখা
যেত?
তাহলে না-মেলা রুটের জন্যও লগিং প্রিন্ট হতো, কারণ মিডলওয়্যার এখন রাউট ম্যাচিং-নির্ভর নয়। বাস্তব ফ্রেমওয়ার্কগুলোতেও কিছু মিডলওয়্যার (যেমন রিকোয়েস্ট-লগিং) ইচ্ছাকৃতভাবে রাউট ম্যাচিং-এর আগে রাখা হয়, যাতে 404 রিকোয়েস্টগুলোও লগ হয় -- এটি একটি ডিজাইন সিদ্ধান্ত, নির্দিষ্ট নিয়ম নয়।
অনুশীলন
-
চিন্তা করুন: "ধাপ ৫ -- রেসপন্স ফরম্যাটিং" ধাপটি কেন হ্যান্ডলারের ভেতরেই না করে আলাদা
রাখা হয়েছে? হ্যান্ডলার নিজেই কেন সরাসরি
{"status": 200, ...}রিটার্ন করে না?যদি প্রতিটি হ্যান্ডলারকে নিজে থেকে status/headers গঠন করতে হতো, তাহলে প্রতিটি হ্যান্ডলারে একই বয়লারপ্লেট কোড বারবার লিখতে হতো এবং কোনো ভুলে দুটো হ্যান্ডলার ভিন্ন আকারে রেসপন্স ফরম্যাট করতে পারত। আলাদা রেসপন্স-ফরম্যাটিং ধাপ রাখলে হ্যান্ডলার শুধু "আসল ফলাফল" (যেমন
{"id": 42, ...}) রিটার্ন করে, আর ফ্রেমওয়ার্ক নিশ্চিত করে প্রতিটি রেসপন্স একই কনসিস্টেন্ট আকারে ক্লায়েন্টের কাছে যায়। -
পরীক্ষা করুন: উপরের কোড সেলে
app-এ একটি নতুন রুট@app.route("GET", "/health")যোগ করে একটিhealth_check(request)ফাংশন লিখুন যা{"status": "ok"}রিটার্ন করে। তারপরapp.process("GET /health HTTP/1.1", {}, None)কল করে Run চেপে দেখুন সবগুলো ধাপ ঠিকভাবে প্রিন্ট হচ্ছে কি না।নতুন রুট রেজিস্টার হলে
self.routes-এ("GET", "/health"): health_checkএন্ট্রি যুক্ত হয়। তখনapp.process(...)কল করলে ধাপ ১ থেকে ৫ পর্যন্ত সবগুলো প্রিন্ট লাইন দেখা যাবে (যেহেতু রুট মিলেছে), এবং চূড়ান্ত রেসপন্সে"body": {"status": "ok"}দেখা যাবে -- ঠিক যেমন/users/42রুটের জন্য হয়েছিল।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
-
পরবর্তী পাঠ: মিডলওয়্যার প্যাটার্ন L19
এই পাঠের "ধাপ ৩ -- মিডলওয়্যার চেইন"-কে
next()কনভেনশনসহ পূর্ণাঙ্গভাবে সম্প্রসারণ করা হবে। - কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
- Python Programming কোর্স সহোদর কোর্স এই পাঠের ডেকোরেটর, ডিকশনারি ও ক্লাস সিনট্যাক্সের ভাষাগত ভিত্তি সেই কোর্সেই তৈরি হয়েছে।