Xử lý Exception trong ASP.NET MVC

Trong hướng dẫn này bạn sẽ tìm hiểu cách xử lý các trường hợp ngoại lệ (Exception) trong ứng dụng ASP.NET MVC.

Bạn có thể xử lý tất cả các trường hợp ngoại lệ có thể có trong các phương thức hành động (action method) bằng cách sử dụng các khối try...catch. Tuy nhiên, có thể có một số trường hợp ngoại lệ chưa được xử lý mà bạn muốn ghi log và hiển thị thông báo lỗi tùy chỉnh hoặc trang lỗi tùy chỉnh cho người dùng.

Khi bạn tạo một ứng dụng ASP.NET MVC trong Visual Studio, nó không thực hiện bất kỳ kỹ thuật xử lý ngoại lệ nào. Nó sẽ hiển thị một trang lỗi khi xảy ra ngoại lệ.

Ví dụ, hãy xem phương thức hành động (action method) sau đây sẽ đưa ra một ngoại lệ.

namespace ExceptionHandlingDemo.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Contact()
        {
            string msg = null;
            ViewBag.Message = msg.Length; // this will throw an exception

            return View();
        }
    }
}

Chuyển hướng đến trang /home/contact trong trình duyệt và bạn sẽ thấy trang sau cho thấy các chi tiết về ngoại lệ đã xảy ra như: kiểu ngoại lệ, vị trí dòng code, tên tập tin nơi xảy ra ngoại lệ và stack trace.

Exception trong ASP.NET MVC

ASP.NET MVC cung cấp các cách sau để xử lý các trường hợp ngoại lệ:

  1. Sử dụng phần tử <customErrors> trong web.config.
  2. Sử dụng thuộc tính nhãn HandleErrorAttribute.
  3. Sử dụng thuộc tính nhãn tùy chỉnh (custom attribute tag).
  4. Ghi đè phương thức Controller.OnException.
  5. Sử dụng sự kiện Application_Error của HttpApplication.

Sử dụng phần tử <customErrors> trong web.config

Cấu hình <customErrors> trong web.config

Phần tử <customErrors> nằm bên trong thẻ system.web trong file web.config được sử dụng để cấu hình mã lỗi thành trang tùy chỉnh.

Nó có thể được sử dụng để cấu hình các trang tùy chỉnh cho bất kỳ mã lỗi 4xx hoặc 5xx.

Tuy nhiên, nó không thể được sử dụng để ghi lại ngoại lệ hoặc thực hiện bất kỳ hành động nào khác về ngoại lệ.

Kích hoạt tính năng <customErrors> trong file web.config, như ví dụ bên dưới.

<system.web> 
    <customErrors mode="On"></customErrors>
</system.web> 

Bạn cũng cần thêm bộ lọc HandleErrorAttribute trong tập tin FilterConfig.cs như sau.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
    }
}

Sau khi bật chế độ customErrors thành On, ứng dụng ASP.NET MVC sẽ hiển thị trang lỗi tùy chỉnh mặc định, như hiển thị trong hình ảnh bên dưới.

Sử dụng phần tử <customErrors> trong web.config để xử lý ngoại lệ trong ASP.NET MVC

Ngạc nhiên chưa! Làm thế nào nó hiển thị trang trên, và trang đó được lấy từ đâu?

Nó đã hiển thị view Error.cshtml từ thư mục Shared. Bộ lọc HandleErrorAttribute thiết lập view lỗi mặc định này trong ASP.NET MVC.

Kiểm tra mã trạng thái của phản hồi trong console dành cho nhà phát triển bằng cách nhấn F12 (trong Chrome) và làm mới lại trang. Bạn sẽ thấy nó đã phản hồi với mã trạng thái 500.

Mã trạng thái 500 khi trả về trang thông báo lỗi trong ASP.NET MVC

Cài đặt này sẽ chỉ hiển thị một trang tùy chỉnh cho các lỗi 500, nhưng không hiển thị cho các mã lỗi khác.

Sử dụng thuộc tính defaultRedirect

Sử dụng thuộc tính defaultRedirect để chỉ định nơi yêu cầu http sẽ được chuyển hướng theo mặc định nếu xảy ra lỗi. Bạn có thể chỉ định tên của web form, HTML hoặc phương thức hành động. Thao tác này sẽ chuyển hướng đến trang thông báo lỗi trên bất kỳ mã lỗi nào, không chỉ 500.

Bạn phải xóa bộ lọc HttpErrorHandler nếu bạn sử dụng thuộc tính defaultRedirect. Nếu không, nó sẽ chuyển hướng đến trang Error.cshtml mặc định, không phải trang đã được cấu hình.

Ví dụ: cấu hình sau sẽ hiển thị trang web form tùy chỉnh Error.aspx trên bất kỳ ngoại lệ 5xx hoặc 4xx nào.

<system.web> 
    <customErrors mode="On" defaultRedirect="Error.aspx" >
    </customErrors>
</system.web> 

Cấu hình sau sẽ hiển thị trang HTML tùy chỉnh Error.html trên bất kỳ ngoại lệ 5xx hoặc 4xx nào.

<system.web> 
    <customErrors mode="On" defaultRedirect="Error.html" >
    </customErrors>
</system.web> 

Bạn cũng có thể chuyển hướng đến phương thức hành động của controller khi xảy ra ngoại lệ. Cài đặt sau sẽ chuyển hướng đến phương thức hành động Index của ErrorController trong ứng dụng ASP.NET MVC.

<system.web> 
    <customErrors mode="On" defaultRedirect="/error" >
    </customErrors>
</system.web> 

Cài đặt trên sẽ hiển thị kết quả sau trong trình duyệt.

Chuyển hướng sang trang lỗi

Lưu ý rằng tất cả các trang lỗi tùy chỉnh được trả về với mã trạng thái 200 và một URL bao gồm chuỗi truy vấn ?aspxerrorpath= trỏ đến trang hoặc phương thức hành động xảy ra ngoại lệ.

Sử dụng các phần tử error con

Bạn cũng có thể đặt các trang hoặc phương thức hành động khác nhau cho các mã lỗi khác nhau bằng cách sử dụng các phần tử <error> con . Phần sau thiết lập các phương thức hành động khác nhau cho các mã lỗi khác nhau.

<customErrors mode="On" defaultRedirect="/error" >
    <error statusCode="400" redirect="/error/badrequest"  />
    <error statusCode="404" redirect="/error/notfound"  />
    <error statusCode="500" redirect="/error/internalerror"  />
</customErrors> 

Tất nhiên, bạn phải tạo tất cả các phương thức hành động ở trên và view của chúng. Các cài đặt trên sẽ hiển thị kết quả sau khi điều hướng đến một trang không tồn tại /home/test.

Như bạn đã thấy, các URL bao gồm chuỗi truy vấn ?aspxerrorpath khi trang lỗi tùy chỉnh hiển thị. Bạn có thể giữ nguyên URL gốc bằng cách sử dụng thuộc tính redirectMode.

Sử dụng thuộc tính redirectMode

Sử dụng thuộc tính redirectMode nếu bạn muốn giữ nguyên URL gốc mà không có chuỗi truy vấn ?aspxerrorpath. Theo mặc định, redirectMode được thiết lập thành ResponseRedirect và đó là lý do tại sao URL bị thay đổi khi xảy ra ngoại lệ.

Thiết lập thuộc tính redirectMode thành ResponseRewrite. Điều này sẽ giữ nguyên URL ban đầu nhưng vẫn hiển thị một trang tùy chỉnh.

Lưu ý rằng ResponseRewrite chỉ áp dụng cho các tập tin .aspx hoặc .html, nhưng không áp dụng cho các phương thức hành động. Nếu bạn muốn thiết lập các phương thức hành động cho các mã trạng thái khác nhau, hãy giữ nguyên giá trị ResponseRedirect.

Phần sau cấu hình các tệp HTML khác nhau cho các mã trạng thái khác nhau và cấu hình redirectMode thành ResponseRewrite.

<customErrors mode="On" redirectMode="ResponseRewrite" defaultRedirect="~/500.html"   >
    <error statusCode="500" redirect="~/500.html" />
    <error statusCode="404" redirect="~/404.html" />
    <error statusCode="400" redirect="~/400.html" />
</customErrors>

Tạo các tệp 500.html, 404.html và 400.html ở thư mục gốc của ứng dụng. Các cài đặt trên sẽ giữ nguyên URL gốc mà không có chuỗi truy vấn trong khi hiển thị nội dung bên trong của trang HTML với mã trạng thái phản hồi 200 OK.

Sử dụng thuộc tính redirectMode

Do đó, bạn có thể sử dụng phần tử <customErrors> trong web.config để hiển thị các trang tùy chỉnh khi các trường hợp ngoại lệ xảy ra trong ứng dụng ASP.NET MVC.

Cấu hình <customErrors> sẽ luôn trả về phản hồi với mã trạng thái 200. Đây không phải là một thực hành SEO tốt cho các trang web. Phần tiếp theo sẽ hướng dẫn bạn trả về phản hồi với mã trạng thái tương ứng.

Sử dụng thuộc tính httpErrors

Bạn có thể sử dụng <customErrors> trong web.config để hiển thị các trang tùy chỉnh khi các trường hợp ngoại lệ xảy ra trong ứng dụng ASP.NET MVC với mã trạng thái phản hồi 200.

Vì lý do SEO, bạn có thể muốn hiển thị trang lỗi tùy chỉnh và trả về mã lỗi thích hợp trong ứng dụng ASP.NET MVC.

Phần tử <customErrors> chỉ được sử dụng để xử lý trường hợp ngoại lệ ở cấp ứng dụng ASP.NET. Có một máy chủ web IIS nằm giữa người dùng và ứng dụng ASP.NET MVC.

Phần tử httpErrors nằm trong thẻ <system.webServer> trong web.config được sử dụng để cấu hình lỗi cấp IIS (IIS 7+). Nó ghi đè cấu hình của phần tử <customErrors>.

Để hiển thị trang lỗi tùy chỉnh với mã lỗi thích hợp, chỉ sử dụng phần tử <httpErrors> và không sử dụng phần tử <customErrors>.

Thêm phần tử <httpErrors> sau vào dưới phần tử <system.webServer>, như hình dưới đây.

<httpErrors errorMode="Custom" existingResponse="Replace"  >
    <remove statusCode="500"/>
    <error statusCode="500" path="500.html" responseMode="File"/>
    <remove statusCode="404"/>
    <error statusCode="404" path="404.html" responseMode="File"/>
    <remove statusCode="400"/>
    <error statusCode="400" path="400.html" responseMode="File"/>
</httpErrors>

Ở trên phần tử <httpErrors> thay thế phản hồi đến từ ASP.NET bằng mã trạng thái phù hợp và trả về tệp HTML tùy chỉnh dưới dạng phản hồi. Thao tác này sẽ bảo toàn URL và trả về trang lỗi tùy chỉnh với mã lỗi thích hợp.

Để cấu hình cho các mã lỗi, hãy tạo tập tin 500.html, 404.html, 400.html ở thư mục gốc của ứng dụng. Sau đây là một tập tin 404.html mẫu.

<!DOCTYPE html>
<html>
<body>
    <h1>Page not found.</h1>
</body>
</html>

Bây giờ, điều hướng đến một trang không tồn tại trong ứng dụng của bạn và bạn sẽ thấy phản hồi sau.

Sử dụng thuộc tính httpErrors

Hãy tìm hiểu các cài đặt <httpErrors> ở trên.

Các thuộc tính errorMode có thể có ba giá trị: DetailedLocalOnly, Detailed và Custom. Chúng ta muốn kiểm tra nó trên localhost, vì vậy đã sử dụng chế độ Custom. Thông thường, bạn nên sử dụng DetailedLocalOnly, nó sẽ hiển thị thông tin lỗi trên localhost nhưng hiển thị trang lỗi khi xuất bản.

Các thuộc tính existingResponse có thể có ba giá trị: Auto, Replace và PassThrough. Vì chúng ta muốn trả lại trang lỗi tùy chỉnh với mã lỗi, chúng ta sẽ phải thay thế phản hồi lỗi đến từ ứng dụng ASP.NET. Vì vậy, hãy sử dụng existingResponse="Replace".

Dòng <remove statusCode="500" /> loại bỏ một thông báo lỗi cụ thể khỏi tập hợp các thông báo lỗi mà trang web hoặc ứng dụng của bạn kế thừa từ cấp cao hơn trong phân cấp cấu hình IIS.

Dòng <error statusCode="500" path="500.html" responseMode="File" /> cấu hình mã lỗi 500. Thuộc tính path xác định đường dẫn đến một trang lỗi tùy chỉnh để trả về. Thuộc tính responseMode chỉ định cụ thể cho IIS phục vụ nội dung tĩnh, nội dung động, hoặc các chuyển hướng đến một URL riêng biệt để đáp ứng với một lỗi.

Phần tử <httpErrors> trên cấu hình ba mã trạng thái 500, 404 và 400. Điều này sẽ trả về trang tùy chỉnh 500.html, 404.html và 400.html tương ứng cho mã trạng thái 500, 404 và 400.

Lưu ý rằng bạn phải tạo các tệp HTML này trong thư mục gốc của ứng dụng.

Bằng cách này, bạn có thể trả lại trang lỗi tùy chỉnh với mã trạng thái thích hợp cho người dùng trong biểu mẫu web ASP.NET hoặc ứng dụng MVC.

Sử dụng thuộc tính nhãn HandleErrorAttribute

HandleErrorAttribute là một thuộc tính nhãn (attribute tag - để tránh nhầm lẫn với property) có thể được sử dụng để xử lý các trường hợp ngoại lệ được ném ra bởi một phương thức hành động hoặc một Controller.

Bạn có thể sử dụng nó để hiển thị view tùy chỉnh về một ngoại lệ cụ thể xảy ra trong một phương thức hành động hoặc trong toàn bộ Controller.

Lưu ý: Thuộc tính nhãn HandleErrorAttribute chỉ có thể được sử dụng để xử lý ngoại lệ với mã trạng thái 500. Ngoài ra, nó không cung cấp cách để ghi log các ngoại lệ.

Để sử dụng thuộc tính nhãn này, bạn phải thêm bộ lọc HandleErrorAttribute trong phương thức FilterConfig.RegisterGlobalFilters() và đồng thời, đặt thuộc tính <customErrors mode="On"> trong web.config, như chúng ta đã làm cho trường hợp customErrors trên.

Bây giờ, hãy sử dụng thuộc tính nhãn [HandleError] cho các phương thức hành động như được trình bày ở bên dưới.

public class HomeController : Controller
{
    [HandleError]
    public ActionResult Contact()
    {
        string msg = null;
        ViewBag.Message = msg.Length;
            
        return View();
    }
}

Ở ví dụ trên, chúng tôi cấu hình thuộc tính nhãn [HandleError] trên phương thức hành động Contact(). Nó sẽ hiển thị view Error.cshtml từ thư mục View -> Shared khi xảy ra ngoại lệ.

Thuộc tính nhãn [HandleError] sử dụng Error.cshtml là view mặc định cho bất kỳ trường hợp ngoại lệ nào.

Thuộc tính nhãn [HandleError] cũng có thể được sử dụng để cấu hình các trang khác nhau với nhiều loại khác nhau của trường hợp ngoại lệ, như hình dưới đây.

public class HomeController : Controller
{
    [HandleError]
    [HandleError(ExceptionType =typeof(NullReferenceException), View ="~/Views/Error/NullReference.cshtml")]
    public ActionResult Contact()
    {
        string msg = null;
        ViewBag.Message = msg.Length;
            
        return View();
    }
}

Bây giờ, ví dụ trên sẽ hiển thị NullReference.cshtml vì nó ném NullReferenceException.

Thuộc tính nhãn [HandleError] có một phạm vi hạn chế và không được khuyến khích để sử dụng trong hầu hết các trường hợp.

Ghi đè phương thức Controller.OnException

Một cách khác để xử lý các trường hợp ngoại lệ ở cấp controller là ghi đè phương thức OnException() trong lớp controller. Phương pháp này xử lý tất cả các lỗi chưa xử lý của bạn với mã lỗi 500.

Nó cho phép bạn ghi log ngoại lệ và chuyển hướng đến view cụ thể. Nó không yêu cầu kích hoạt cấu hình <customErrors> trong web.config.

public class HomeController : Controller
{
    public ActionResult Contact()
    {
        string msg = null;
        ViewBag.Message = msg.Length;
            
        return View();
    }
    
    protected override void OnException(ExceptionContext filterContext)
    {
        filterContext.ExceptionHandled = true;

        //Log the error!!
     
        //Redirect to action
        filterContext.Result = RedirectToAction("Error", "InternalError");

        // OR return specific view
        filterContext.Result = new ViewResult
        {
            ViewName = "~/Views/Error/InternalError.cshtml"
        };
    }
} 

Sử dụng sự kiện Application_Error của HttpApplication

Cách lý tưởng để ghi lại ngoại lệ xảy ra trong bất kỳ phần nào trong ứng dụng ASP.NET MVC của bạn là xử lý nó trong sự kiện Application_Error trong tập tin global.asax.

public class MvcApplication : System.Web.HttpApplication
{
    //other code removed for clarity

    protected void Application_Error()
    {
        var ex = Server.GetLastError();
        //log an exception
    }
}

Sự kiện Application_Error này xảy ra trên bất kỳ loại ngoại lệ và mã lỗi nào. Vì vậy, hãy xử lý nó một cách cẩn thận.

Sử dụng thuộc tính nhãn tùy chỉnh

Đây là cách mình áp dụng trong các dự án thực tế để ghi log tất cả các ngoại lệ (sử dụng log4net) và xử lý lỗi phù hợp với hoàn cảnh:

  • Nếu là AJAX request thì trả về JavaScriptResult kèm thông báo lỗi để hiện thị một popup thông báo lỗi.
  • Ngược lại sẽ chuyển hướng người dùng sang trang thông báo lỗi.

Để thực hiện điều này, đầu tiên bạn tạo một thuộc tính nhãn tùy chỉnh (custom attribute tag) là CustomHandleErrorAttribute kế thừa từ lớp HandleErrorAttribute như sau:

[AttributeUsage(AttributeTargets.Class)]
public sealed class CustomHandleErrorAttribute : HandleErrorAttribute
{
    private readonly ILogger _log = LogManager.GetLogger();

    public override void OnException(ExceptionContext exceptionContext)
    {
        exceptionContext.ExceptionHandled = true;
        exceptionContext.HttpContext.Response.Clear();

        var original = exceptionContext.Exception.InnerException ?? exceptionContext.Exception;
        var errorMessage = original.Message;

        LogException(exceptionContext);

        if (exceptionContext.HttpContext.Request.IsAjaxRequest())
        {
            var message = "This is an error message";
            exceptionContext.Result = new JavaScriptResult { Script = $"comdy.common.ajaxError('{message}');" };
            return;
        }

        // stores the error message in TempData and displays it on the Error page
        exceptionContext.Controller.TempData["message"] = HttpUtility.UrlDecode(errorMessage);
        exceptionContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Error", action = "Index" }));
    }

    private void LogException(ExceptionContext exceptionContext)
    {
        var builder = new StringBuilder();
        
        // build error message here

        _log.Error(builder.ToString(), exceptionContext.Exception);
    }
}

Tiếp theo bạn cần tắt tính năng <customErrors> trong file web.config, như ví dụ bên dưới.

<system.web> 
    <customErrors mode="Off"></customErrors>
</system.web> 

Sau đó bạn cần đăng ký CustomHandleErrorAttribute trong tập tin FilterConfig.cs như sau.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new CustomHandleErrorAttribute());
    }
}

Bạn tạo một lớp controller cơ sở tên là BaseController, tất cả các controller trong ứng dụng ASP.NET MVC của bạn sẽ kế thừa từ controller cơ sở này. Trong controller cơ sở này chúng ta sẽ khai báo thuộc tính nhãn CustomHandleErrorAttribute.

[CustomHandleError]
protected abstract class BaseController : AsyncController
{
    protected BaseController()
    { }
}

Bây giờ, mỗi khi tạo controller mới bạn chỉ cần kế thừa từ BaseController này là xong. Tất cả lỗi trong hệ thống sẽ được ghi log và xử lý phù hợp theo hoàn cảnh.

Phần kết luận

Trong hầu hết các ứng dụng web, bạn nên ghi log các trường hợp ngoại lệ một cách thích hợp và cũng hiển thị các thông báo lỗi hoặc trang phù hợp cho người dùng.

Qua hướng dẫn này mình hi vọng sẽ giúp bạn hiểu các giải pháp xử lý ngoại lệ trong ứng dụng ASP.NET MVC và tìm ra cho mình giải pháp phù hợp nhất.

ASP.NET MVC
Bài Viết Liên Quan:
Bundle và minify js, css trong ASP.NET MVC
Trung Nguyen 11/04/2021
Bundle và minify js, css trong ASP.NET MVC

Các kỹ thuật bundle (gộp file) và minify (rút gọn file) đã được giới thiệu trong ASP.NET MVC 4 để cải thiện thời gian tải trang.

Filter trong ASP.NET MVC
Trung Nguyen 10/04/2021
Filter trong ASP.NET MVC

Trong hướng dẫn này, bạn sẽ tìm hiểu filter trong ASP.NET MVC là gì? Có những loại filter nào và cách sử dụng filter trong ASP.NET MVC.

ViewBag, ViewData và TempData trong ASP.NET MVC
Trung Nguyen 10/04/2021
ViewBag, ViewData và TempData trong ASP.NET MVC

Trong hướng dẫn này, bạn sẽ tìm hiểu ViewBag, ViewData và TempData là gì và cách sử dụng chúng trong ASP.NET MVC

Layout View trong ASP.NET MVC
Trung Nguyen 10/04/2021
Layout View trong ASP.NET MVC

Trong hướng dẫn này, bạn sẽ tìm hiểu về Layout View là gì các cách sử dụng Layout View trong ASP.NET MVC.