عنوان:

‫درج await _next(context) را در میان‌افزارها (Middleware)، فراموش نکنید!


نویسنده: هادی مزارعی
تاریخ: ۱۴۰۴/۰۵/۰۱ ۱۲:۴۵
آدرس: www.dntips.ir
در یک پروژه Web Api یک Custom Permission Middleware ایجاد کردم که فاکتورهای زیادی را بررسی می‌کرد و همه چیز خوب بود. البته همیشه با حساب کاربری Developer وارد میشدم تا در زمان بررسی و توسعه برنامه، هیچ محدودیتی نداشته باشم. بعد مدت‌ها گفتم با حساب کاربر عادی (Simple User) وارد بشم تا ببینم محدودیت‌ها چطور اعمال می‌شوند که در همان فرم لاگین و بعد از احراز هویت کاربر، اطلاعاتی که برمبنای کاربر Authenticate شده باید دریافت شود بی‌پاسخ می‌ماند ولی context در پاسخ بازگشتی از HttpClient کد وضعیت را StatusCode = 200 برگشت می‌داد و Content خالی بود. جالبتر اینکه Break Pointی که برای متد GET در Controller قرار داده بودم هرگز توسط Debugger فعال نمی‌شد و برایم خیلی عجیب بود.

مساله حل شد:
تمام کدهای داخل بلوک Invoke در Middleware را بررسی کردم و همه چیز درست بود. زمانیکه با حساب Developer وارد میشدم همان ابتدای کار درخواست با استفاده از await _next(context) به Middleware بعدی ارسال میشد و خط بعدی return بود. اما برای کاربر عادی باید تا انتهای کدهای Invoke اجرا می‌شد و هرجا که دسترسی وجود نداشت return می‌شد و این مورد هم درست کار میکرد چون نتیجه بررسی‌ها، عدم دسترسی را نشان می‌داد. اما اشتباه من این بود که فکر میکردم اگر Invoke به انتها برسد، حتما Middleware بعدی اجرا می‌شود که هیچ وقت نشد که بعد صرف حدود نیم روز و اصلاح کلی کد متوجه شدم در انتها، اگر پارامتری، context را return نکرد باید در انتها next اجرا شود.

پیشنهاد:
در بلوک Invoke تمام کدها را در بالای عبارت await _next(context) بنویسید تا همیشه این عبارت در انتها خط وجود داشته باشد مگر اینکه آگاهانه حذفش کنید.

  • Middleware به مثابه یک زنجیره: Middleware در ASP.NET Core مانند حلقه‌های یک زنجیر عمل می‌کند که به آن "خط لوله پردازش درخواست" (Request Pipeline) می‌گویند. هر درخواست از اولین حلقه (Middleware) شروع می‌کند و باید به صورت دستی به حلقه بعدی منتقل شود.
  • نقش await _next(context): این دستور دقیقاً همان "دست به دست کردن" درخواست به حلقه بعدی زنجیر است. اگر این دستور در یک مسیر منطقی از کد شما فراخوانی نشود، زنجیره در همان نقطه متوقف (short-circuited) می‌شود.
  • چرا StatusCode 200 با محتوای خالی؟ چون Middleware ما به صراحت خطایی (مانند 401 یا 403) را به context.Response اضافه نکرده بود، ASP.NET Core فرض را بر این می‌گذاشت که پردازش با موفقیت به پایان رسیده است. اما چون درخواست هرگز به Controller نرسیده بود تا پاسخی تولید کند، Content خالی بازگردانده می‌شد. در واقع، Middleware ما بدون تولید پاسخ، مسئولیت را به پایان رساند.


public async Task InvokeAsync(HttpContext context)
{
    // اگر کاربر ادمین است، بدون بررسی به مرحله بعد برو
    if (IsAdmin(context.User))
    {
        await _next(context);
        return;
    }

    // در اینجا منطق بررسی دسترسی برای کاربر عادی قرار می‌گیرد
    // اگر کاربر دسترسی نداشت، می‌توانستید با تنظیم StatusCode و بازگشت، زنجیره را متوقف کنید
    // if (!HasAccess(context.User))
    // {
    //     context.Response.StatusCode = 403; // Forbidden
    //     return;
    // }

    // اگر تمام بررسی‌ها برای کاربر عادی موفق بود، درخواست به مرحله بعد می‌رود
    // این همان خطی بود که در کد ما برای کاربر عادی اجرا نمی‌شد
    await _next(context);
}