পাঠ ১৯ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Full-Stack Web Frameworks / মিডলওয়্যার

মিডলওয়্যার প্যাটার্ন

The middleware pattern
১০ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • মিডলওয়্যার প্যাটার্ন কী এবং L18-এর "ধাপ ৩ -- মিডলওয়্যার চেইন"-কে কীভাবে এটি সম্পূর্ণ করে
  • next() কনভেনশন — মিডলওয়্যার কীভাবে চেইনের নিয়ন্ত্রণ নেয় ও ছেড়ে দেয়
  • একটি অথ-চেক মিডলওয়্যার কীভাবে ব্যর্থ হলে চেইন শর্ট-সার্কিট করে হ্যান্ডলার পর্যন্ত পৌঁছাতে দেয় না
  • একই চেইনে অনুমোদিত ও অননুমোদিত রিকোয়েস্টের জন্য ভিন্ন এক্সিকিউশন পাথ বাস্তবে প্রমাণ করা

১ · মিডলওয়্যার কী এবং next() কনভেনশন

L18-এ দেখা হয়েছিল মিডলওয়্যার চেইন রাউট ম্যাচিং ও হ্যান্ডলারের মাঝখানে ক্রমানুসারে চলে। কিন্তু সেই সংস্করণে প্রতিটি মিডলওয়্যার বাধ্যতামূলকভাবে চলত — কোনো মিডলওয়্যারের চেইন থামিয়ে দেওয়ার ক্ষমতা ছিল না। বাস্তব ফ্রেমওয়ার্কে (Express, Django, ইত্যাদি) প্রতিটি মিডলওয়্যার একটি next ফাংশন গ্রহণ করে, এবং নিজে সিদ্ধান্ত নেয় চেইন চালিয়ে যাবে কি না।

next_fn(request) কল করা হলে
চেইনের পরবর্তী মিডলওয়্যার (বা শেষে হ্যান্ডলার) চলে, এবং তার রেসপন্স ফিরে আসে।
next_fn(request) কল না করা হলে
চেইন এখানেই থেমে যায় — বাকি সব মিডলওয়্যার ও হ্যান্ডলার কখনো চলে না (শর্ট-সার্কিট)।
logging_middleware next() কল করে auth_middleware টোকেন যাচাই বৈধ final_handler শুধু বৈধ হলে চলে অবৈধ -- next() কল হয় না 401 রেসপন্স handler কখনো চলে না
টোকেন অবৈধ হলে auth_middleware নিজেই একটি 401 রেসপন্স রিটার্ন করে, next_fn() কখনো কল করে না -- ফলে final_handler কখনো চলে না।

২ · একটি real মিডলওয়্যার চেইন — কোড দিয়ে

নিচের কোডে build_chain() ফাংশনটি একটি মিডলওয়্যার লিস্ট ও একটি ফাইনাল হ্যান্ডলার নিয়ে একটি চেইন তৈরি করে। প্রতিটি মিডলওয়্যারকে তার নিজস্ব next_fn সরবরাহ করা হয় — যেটি কল করলে চেইনের পরের ধাপে যায়। একটি এক্সিকিউশন ট্রেস লিস্টও রাখা হয়েছে, যাতে "কোন কোন ফাংশন আসলে চলেছে" তা সরাসরি প্রমাণ করা যায়।

Python
# চেইন-বিল্ডার -- প্রতিটি মিডলওয়্যারকে তার নিজের next_fn সরবরাহ করে
def build_chain(middlewares, final_handler):
    def dispatch(index, request, trace):
        if index == len(middlewares):
            trace.append(final_handler.__name__)
            return final_handler(request)
        current_mw = middlewares[index]
        def next_fn(req):
            return dispatch(index + 1, req, trace)
        trace.append(current_mw.__name__)
        return current_mw(request, next_fn)

    def run(request):
        trace = []
        response = dispatch(0, request, trace)
        return response, trace

    return run


# --- মিডলওয়্যার ১: লগিং -- সবসময় next() কল করে ---
def logging_middleware(request, next_fn):
    print(f"[logging] {request['method']} {request['path']} রিকোয়েস্ট শুরু")
    response = next_fn(request)
    print(f"[logging] রেসপন্স status={response['status']}")
    return response


# --- মিডলওয়্যার ২: অথ-চেক -- শর্তসাপেক্ষে next() কল করে ---
VALID_TOKEN = "Bearer secret-token"

def auth_middleware(request, next_fn):
    token = request.get("headers", {}).get("Authorization")
    if token != VALID_TOKEN:
        print("[auth] টোকেন অবৈধ -- next() কল হয়নি, চেইন থেমে গেছে")
        return {"status": 401, "body": "Unauthorized"}
    print("[auth] টোকেন বৈধ -- next() কল হচ্ছে")
    return next_fn(request)


# --- চূড়ান্ত হ্যান্ডলার ---
def final_handler(request):
    print("[handler] ড্যাশবোর্ড হ্যান্ডলার চলছে")
    return {"status": 200, "body": "স্বাগতম, ড্যাশবোর্ড ডেটা"}


run_chain = build_chain([logging_middleware, auth_middleware], final_handler)

authorized_request = {
    "method": "GET", "path": "/dashboard",
    "headers": {"Authorization": VALID_TOKEN},
}
unauthorized_request = {
    "method": "GET", "path": "/dashboard",
    "headers": {"Authorization": "Bearer wrong-token"},
}

print("=== একই চেইনে: অনুমোদিত রিকোয়েস্ট ===")
response1, trace1 = run_chain(authorized_request)
print("চলেছে (trace):", trace1)
print("চূড়ান্ত ফলাফল:", response1)

print("\n=== একই চেইনে: অননুমোদিত রিকোয়েস্ট ===")
response2, trace2 = run_chain(unauthorized_request)
print("চলেছে (trace):", trace2)
print("চূড়ান্ত ফলাফল:", response2)

print("\nহ্যান্ডলার চলেছিল কি? অনুমোদিত:", "final_handler" in trace1, "| অননুমোদিত:", "final_handler" in trace2)

    
trace1 হবে ['logging_middleware', 'auth_middleware', 'final_handler'] — তিনটিই চলেছে। কিন্তু trace2 হবে শুধু ['logging_middleware', 'auth_middleware'] — final_handler নামটি সেখানে নেই, কারণ auth_middleware টোকেন অবৈধ পেয়ে কখনো next_fn(request) কল করেনি, ফলে dispatch() কখনো index == len(middlewares) ধাপে পৌঁছায়নি। এটিই "শর্ট-সার্কিট"-এর প্রমাণ — শুধু বলা হচ্ছে না, সরাসরি ট্রেস লিস্টে দেখা যাচ্ছে।

৩ · মিডলওয়্যারের ক্রম কেন গুরুত্বপূর্ণ

উপরের চেইনে logging_middleware সবসময় প্রথমে থাকে বলেই এটি প্রতিটি রিকোয়েস্টের (এমনকি অননুমোদিত রিকোয়েস্টেরও) শুরু ও শেষ — দুটোই লগ করতে পারে। যদি auth_middleware-কে প্রথমে রাখা হতো, তাহলে অননুমোদিত রিকোয়েস্টের ক্ষেত্রে logging_middleware কখনোই চলত না — লগ ফাইলে সেই ব্যর্থ প্রচেষ্টাটির কোনো রেকর্ডই থাকত না। মিডলওয়্যারের ক্রম তাই শুধু কোড-অর্ডার নয়, একটি প্রকৃত আর্কিটেকচারাল সিদ্ধান্ত।

মূল কথা · Key takeaway

মিডলওয়্যার প্যাটার্নে প্রতিটি ফাংশন (request, next_fn) নেয় এবং নিজেই সিদ্ধান্ত নেয় চেইন চালিয়ে যাবে (next_fn(request) কল করে) নাকি সেখানেই থামিয়ে একটি রেসপন্স রিটার্ন করবে। একই চেইন একেক রিকোয়েস্টে একেকভাবে আচরণ করতে পারে — এটিই এই প্যাটার্নের শক্তি, এবং এটিই L22-এ এরর-হ্যান্ডলিং মিডলওয়্যার তৈরির ভিত্তি হবে।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ logging_middleware-এ next_fn(request) কল করার পরও কেন আরেকটি print() লাইন আছে?

কারণ next_fn(request) একটি সাধারণ ফাংশন কল — এটি রিটার্ন করার পরই পরবর্তী লাইন চলে। তাই response = next_fn(request) লাইনটি ততক্ষণ থামে না যতক্ষণ না চেইনের বাকি সব (অথ-চেক, হ্যান্ডলার) সম্পূর্ণ হয়ে একটি চূড়ান্ত রেসপন্স ফেরত না দেয়। তাই next_fn()-এর পরের print() আসলে চলে "রেসপন্স পথে ফেরার সময়" — এটি অনেকটা রিকার্সিভ ফাংশনের মতো আচরণ।

প্র ০২ যদি auth_middleware ভুলবশত if/else দুটো শাখাতেই next_fn(request) কল করত, তাহলে কী সমস্যা হতো?

তাহলে টোকেন অবৈধ হলেও চেইন final_handler পর্যন্ত পৌঁছে যেত -- অথেন্টিকেশন চেক থাকলেও কার্যত কোনো সুরক্ষা দিত না, যেকোনো অননুমোদিত রিকোয়েস্টও ড্যাশবোর্ড ডেটা পেয়ে যেত। এটি একটি বাস্তব ও সাধারণ বাগ -- মিডলওয়্যার লেখার সময় "ব্যর্থ হলে next() কল না করা" নিশ্চিত করা অত্যন্ত গুরুত্বপূর্ণ।

প্র ০৩ trace লিস্টে final_handler "আছে কি নেই" পরীক্ষা করাটা কেন response["status"] চেক করার চেয়ে বেশি নির্ভরযোগ্য প্রমাণ?

response["status"] শুধু চূড়ান্ত ফলাফল দেখায় -- কে সেটা তৈরি করেছে তা বলে না। কোনো মিডলওয়্যার ভুলবশত নিজেই একটি 200 রেসপন্স তৈরি করলেও status কোড মিলে যেতে পারত। কিন্তু trace লিস্টে সরাসরি রেকর্ড থাকে ঠিক কোন কোন ফাংশন আসলে চলেছে -- এটি এক্সিকিউশন পাথের একটি সরাসরি, hand-traceable প্রমাণ, শুধু অনুমান নয়।

অনুশীলন

  1. চিন্তা করুন: রিয়েল ফ্রেমওয়ার্কে (যেমন Express) একটি রিকোয়েস্ট-বডি সাইজ সীমিত করার মিডলওয়্যার কোথায় বসানো উচিত -- চেইনের শুরুতে, নাকি শেষে? কেন?

    চেইনের শুরুতে -- কারণ যদি রিকোয়েস্ট বডি অনুমোদিত সীমার চেয়ে বড় হয়, তাহলে বাকি কোনো মিডলওয়্যার (অথ-চেক, লগিং বিস্তারিত পার্সিং ইত্যাদি) চালানোর আগেই সেটি প্রত্যাখ্যান করা উচিত -- অন্যথায় সার্ভার অপ্রয়োজনীয়ভাবে বড় পেলোড প্রসেস করতে সময়/মেমরি ব্যয় করবে, যা একটি সহজ denial-of-service ভেক্টর হতে পারে।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় মিডলওয়্যার rate_limit_middleware(request, next_fn) লিখুন যা request["path"] == "/dashboard" হলে সবসময় next_fn(request) কল করে (কোনো লিমিট নেই ধরে নিন), কিন্তু একটি print("[rate-limit] পাস") লাইন যোগ করুন। এটিকে build_chain([logging_middleware, rate_limit_middleware, auth_middleware], final_handler)-এ রেখে Run চেপে দেখুন trace1-এ এখন কয়টি নাম আছে।

    নতুন মিডলওয়্যারটিও সবসময় next_fn() কল করে বলে চেইনে বাধা দেয় না, তাই অনুমোদিত রিকোয়েস্টের জন্য trace1 এখন চারটি নাম ধারণ করবে: ['logging_middleware', 'rate_limit_middleware', 'auth_middleware', 'final_handler'] -- চেইনের দৈর্ঘ্য বাড়লেও build_chain()-এর লজিক অপরিবর্তিত থাকে, কারণ এটি মিডলওয়্যার-লিস্টের দৈর্ঘ্য অনুযায়ী স্বয়ংক্রিয়ভাবে কাজ করে।

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

আগের পাঠ
রাউটিং ও রিকোয়েস্ট লাইফসাইকেল