How can I properly handle 404 in ASP.NET MVC?

后端 未结 19 2694
爱一瞬间的悲伤
爱一瞬间的悲伤 2020-11-21 10:14

I am using RC2

Using URL Routing:

routes.MapRoute(
    \"Error\",
     \"{*url}\",
     new { controller = \"Errors\", action = \"N         


        
相关标签:
19条回答
  • 2020-11-21 10:25

    Posting an answer since my comment was too long...

    It's both a comment and questions to the unicorn post/answer:

    https://stackoverflow.com/a/7499406/687549

    I prefer this answer over the others for it's simplicity and the fact that apparently some folks at Microsoft were consulted. I got three questions however and if they can be answered then I will call this answer the holy grail of all 404/500 error answers on the interwebs for an ASP.NET MVC (x) app.

    @Pure.Krome

    1. Can you update your answer with the SEO stuff from the comments pointed out by GWB (there was never any mentioning of this in your answer) - <customErrors mode="On" redirectMode="ResponseRewrite"> and <httpErrors errorMode="Custom" existingResponse="Replace">?

    2. Can you ask your ASP.NET team friends if it is okay to do it like that - would be nice to have some confirmation - maybe it's a big no-no to change redirectMode and existingResponse in this way to be able to play nicely with SEO?!

    3. Can you add some clarification surrounding all that stuff (customErrors redirectMode="ResponseRewrite", customErrors redirectMode="ResponseRedirect", httpErrors errorMode="Custom" existingResponse="Replace", REMOVE customErrors COMPLETELY as someone suggested) after talking to your friends at Microsoft?

    As I was saying; it would be supernice if we could make your answer more complete as this seem to be a fairly popular question with 54 000+ views.

    Update: Unicorn answer does a 302 Found and a 200 OK and cannot be changed to only return 404 using a route. It has to be a physical file which is not very MVC:ish. So moving on to another solution. Too bad because this seemed to be the ultimate MVC:ish answer this far.

    0 讨论(0)
  • 2020-11-21 10:26

    Quick Answer / TL;DR

    enter image description here

    For the lazy people out there:

    Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0
    

    Then remove this line from global.asax

    GlobalFilters.Filters.Add(new HandleErrorAttribute());
    

    And this is only for IIS7+ and IIS Express.

    If you're using Cassini .. well .. um .. er.. awkward ... awkward


    Long, explained answer

    I know this has been answered. But the answer is REALLY SIMPLE (cheers to David Fowler and Damian Edwards for really answering this).

    There is no need to do anything custom.

    For ASP.NET MVC3, all the bits and pieces are there.

    Step 1 -> Update your web.config in TWO spots.

    <system.web>
        <customErrors mode="On" defaultRedirect="/ServerError">
          <error statusCode="404" redirect="/NotFound" />
        </customErrors>
    

    and

    <system.webServer>
        <httpErrors errorMode="Custom">
          <remove statusCode="404" subStatusCode="-1" />
          <error statusCode="404" path="/NotFound" responseMode="ExecuteURL" />
          <remove statusCode="500" subStatusCode="-1" />
          <error statusCode="500" path="/ServerError" responseMode="ExecuteURL" />
        </httpErrors>    
    
    ...
    <system.webServer>
    ...
    </system.web>
    

    Now take careful note of the ROUTES I've decided to use. You can use anything, but my routes are

    • /NotFound <- for a 404 not found, error page.
    • /ServerError <- for any other error, include errors that happen in my code. this is a 500 Internal Server Error

    See how the first section in <system.web> only has one custom entry? The statusCode="404" entry? I've only listed one status code because all other errors, including the 500 Server Error (ie. those pesky error that happens when your code has a bug and crashes the user's request) .. all the other errors are handled by the setting defaultRedirect="/ServerError" .. which says, if you are not a 404 page not found, then please goto the route /ServerError.

    Ok. that's out of the way.. now to my routes listed in global.asax

    Step 2 - Creating the routes in Global.asax

    Here's my full route section..

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("{*favicon}", new {favicon = @"(.*/)?favicon.ico(/.*)?"});
    
        routes.MapRoute(
            "Error - 404",
            "NotFound",
            new { controller = "Error", action = "NotFound" }
            );
    
        routes.MapRoute(
            "Error - 500",
            "ServerError",
            new { controller = "Error", action = "ServerError"}
            );
    
        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new {controller = "Home", action = "Index", id = UrlParameter.Optional}
            );
    }
    

    That lists two ignore routes -> axd's and favicons (ooo! bonus ignore route, for you!) Then (and the order is IMPERATIVE HERE), I have my two explicit error handling routes .. followed by any other routes. In this case, the default one. Of course, I have more, but that's special to my web site. Just make sure the error routes are at the top of the list. Order is imperative.

    Finally, while we are inside our global.asax file, we do NOT globally register the HandleError attribute. No, no, no sir. Nadda. Nope. Nien. Negative. Noooooooooo...

    Remove this line from global.asax

    GlobalFilters.Filters.Add(new HandleErrorAttribute());
    

    Step 3 - Create the controller with the action methods

    Now .. we add a controller with two action methods ...

    public class ErrorController : Controller
    {
        public ActionResult NotFound()
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return View();
        }
    
        public ActionResult ServerError()
        {
            Response.StatusCode = (int)HttpStatusCode.InternalServerError;
    
            // Todo: Pass the exception into the view model, which you can make.
            //       That's an exercise, dear reader, for -you-.
            //       In case u want to pass it to the view, if you're admin, etc.
            // if (User.IsAdmin) // <-- I just made that up :) U get the idea...
            // {
            //     var exception = Server.GetLastError();
            //     // etc..
            // }
    
            return View();
        }
    
        // Shhh .. secret test method .. ooOOooOooOOOooohhhhhhhh
        public ActionResult ThrowError()
        {
            throw new NotImplementedException("Pew ^ Pew");
        }
    }
    

    Ok, lets check this out. First of all, there is NO [HandleError] attribute here. Why? Because the built in ASP.NET framework is already handling errors AND we have specified all the shit we need to do to handle an error :) It's in this method!

    Next, I have the two action methods. Nothing tough there. If u wish to show any exception info, then u can use Server.GetLastError() to get that info.

    Bonus WTF: Yes, I made a third action method, to test error handling.

    Step 4 - Create the Views

    And finally, create two views. Put em in the normal view spot, for this controller.

    enter image description here

    Bonus comments

    • You don't need an Application_Error(object sender, EventArgs e)
    • The above steps all work 100% perfectly with Elmah. Elmah fraking wroxs!

    And that, my friends, should be it.

    Now, congrats for reading this much and have a Unicorn as a prize!

    enter image description here

    0 讨论(0)
  • 2020-11-21 10:26

    I went through most of the solutions posted on this thread. While this question might be old, it is still very applicable to new projects even now, so I spent quite a lot of time reading up on the answers presented here as well as else where.

    As @Marco pointed out the different cases under which a 404 can happen, I checked the solution I compiled together against that list. In addition to his list of requirements, I also added one more.

    • The solution should be able to handle MVC as well as AJAX/WebAPI calls in the most appropriate manner. (i.e. if 404 happens in MVC, it should show the Not Found page and if 404 happens in WebAPI, it should not hijack the XML/JSON response so that the consuming Javascript can parse it easily).

    This solution is 2 fold:

    First part of it comes from @Guillaume at https://stackoverflow.com/a/27354140/2310818. Their solution takes care of any 404 that were caused due to invalid route, invalid controller and invalid action.

    The idea is to create a WebForm and then make it call the NotFound action of your MVC Errors Controller. It does all of this without any redirect so you will not see a single 302 in Fiddler. The original URL is also preserved, which makes this solution fantastic!


    Second part of it comes from @Germán at https://stackoverflow.com/a/5536676/2310818. Their solution takes care of any 404 returned by your actions in the form of HttpNotFoundResult() or throw new HttpException()!

    The idea is to have a filter look at the response as well as the exception thrown by your MVC controllers and to call the appropriate action in your Errors Controller. Again this solution works without any redirect and the original url is preserved!


    As you can see, both of these solutions together offer a very robust error handling mechanism and they achieve all the requirements listed by @Marco as well as my requirements. If you would like to see a working sample or a demo of this solution, please leave in the comments and I would be happy to put it together.

    0 讨论(0)
  • 2020-11-21 10:27

    Requirements for 404

    The following are my requirements for a 404 solution and below i show how i implement it:

    • I want to handle matched routes with bad actions
    • I want to handle matched routes with bad controllers
    • I want to handle un-matched routes (arbitrary urls that my app can't understand) - i don't want these bubbling up to the Global.asax or IIS because then i can't redirect back into my MVC app properly
    • I want a way to handle in the same manner as above, custom 404s - like when an ID is submitted for an object that does not exist (maybe deleted)
    • I want all my 404s to return an MVC view (not a static page) to which i can pump more data later if necessary (good 404 designs) and they must return the HTTP 404 status code

    Solution

    I think you should save Application_Error in the Global.asax for higher things, like unhandled exceptions and logging (like Shay Jacoby's answer shows) but not 404 handling. This is why my suggestion keeps the 404 stuff out of the Global.asax file.

    Step 1: Have a common place for 404-error logic

    This is a good idea for maintainability. Use an ErrorController so that future improvements to your well designed 404 page can adapt easily. Also, make sure your response has the 404 code!

    public class ErrorController : MyController
    {
        #region Http404
    
        public ActionResult Http404(string url)
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            var model = new NotFoundViewModel();
            // If the url is relative ('NotFound' route) then replace with Requested path
            model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ?
                Request.Url.OriginalString : url;
            // Dont get the user stuck in a 'retry loop' by
            // allowing the Referrer to be the same as the Request
            model.ReferrerUrl = Request.UrlReferrer != null &&
                Request.UrlReferrer.OriginalString != model.RequestedUrl ?
                Request.UrlReferrer.OriginalString : null;
    
            // TODO: insert ILogger here
    
            return View("NotFound", model);
        }
        public class NotFoundViewModel
        {
            public string RequestedUrl { get; set; }
            public string ReferrerUrl { get; set; }
        }
    
        #endregion
    }
    

    Step 2: Use a base Controller class so you can easily invoke your custom 404 action and wire up HandleUnknownAction

    404s in ASP.NET MVC need to be caught at a number of places. The first is HandleUnknownAction.

    The InvokeHttp404 method creates a common place for re-routing to the ErrorController and our new Http404 action. Think DRY!

    public abstract class MyController : Controller
    {
        #region Http404 handling
    
        protected override void HandleUnknownAction(string actionName)
        {
            // If controller is ErrorController dont 'nest' exceptions
            if (this.GetType() != typeof(ErrorController))
                this.InvokeHttp404(HttpContext);
        }
    
        public ActionResult InvokeHttp404(HttpContextBase httpContext)
        {
            IController errorController = ObjectFactory.GetInstance<ErrorController>();
            var errorRoute = new RouteData();
            errorRoute.Values.Add("controller", "Error");
            errorRoute.Values.Add("action", "Http404");
            errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
            errorController.Execute(new RequestContext(
                 httpContext, errorRoute));
    
            return new EmptyResult();
        }
    
        #endregion
    }
    

    Step 3: Use Dependency Injection in your Controller Factory and wire up 404 HttpExceptions

    Like so (it doesn't have to be StructureMap):

    MVC1.0 example:

    public class StructureMapControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(Type controllerType)
        {
            try
            {
                if (controllerType == null)
                    return base.GetControllerInstance(controllerType);
            }
            catch (HttpException ex)
            {
                if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
                {
                    IController errorController = ObjectFactory.GetInstance<ErrorController>();
                    ((ErrorController)errorController).InvokeHttp404(RequestContext.HttpContext);
    
                    return errorController;
                }
                else
                    throw ex;
            }
    
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    }
    

    MVC2.0 example:

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            try
            {
                if (controllerType == null)
                    return base.GetControllerInstance(requestContext, controllerType);
            }
            catch (HttpException ex)
            {
                if (ex.GetHttpCode() == 404)
                {
                    IController errorController = ObjectFactory.GetInstance<ErrorController>();
                    ((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext);
    
                    return errorController;
                }
                else
                    throw ex;
            }
    
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    

    I think its better to catch errors closer to where they originate. This is why i prefer the above to the Application_Error handler.

    This is the second place to catch 404s.

    Step 4: Add a NotFound route to Global.asax for urls that fail to be parsed into your app

    This route should point to our Http404 action. Notice the url param will be a relative url because the routing engine is stripping the domain part here? That is why we have all that conditional url logic in Step 1.

            routes.MapRoute("NotFound", "{*url}", 
                new { controller = "Error", action = "Http404" });
    

    This is the third and final place to catch 404s in an MVC app that you don't invoke yourself. If you don't catch unmatched routes here then MVC will pass the problem up to ASP.NET (Global.asax) and you don't really want that in this situation.

    Step 5: Finally, invoke 404s when your app can't find something

    Like when a bad ID is submitted to my Loans controller (derives from MyController):

        //
        // GET: /Detail/ID
    
        public ActionResult Detail(int ID)
        {
            Loan loan = this._svc.GetLoans().WithID(ID);
            if (loan == null)
                return this.InvokeHttp404(HttpContext);
            else
                return View(loan);
        }
    

    It would be nice if all this could be hooked up in fewer places with less code but i think this solution is more maintainable, more testable and fairly pragmatic.

    Thanks for the feedback so far. I'd love to get more.

    NOTE: This has been edited significantly from my original answer but the purpose/requirements are the same - this is why i have not added a new answer

    0 讨论(0)
  • 2020-11-21 10:28

    I have gone through all articles but nothing works for me: My requirement user type anything in your url custom 404 page should show.I thought it is very straight forward.But you should understand handling of 404 properly:

     <system.web>
        <customErrors mode="On" redirectMode="ResponseRewrite">
          <error statusCode="404" redirect="~/PageNotFound.aspx"/>
        </customErrors>
      </system.web>
    <system.webServer>
        <httpErrors errorMode="Custom">
          <remove statusCode="404"/>
          <error statusCode="404" path="/PageNotFound.html" responseMode="ExecuteURL"/>
        </httpErrors>
    </system.webServer>
    

    I found this article very helpfull.should be read at once.Custome error page-Ben Foster

    0 讨论(0)
  • 2020-11-21 10:31

    Dealing with errors in ASP.NET MVC is just a pain in the butt. I tried a whole lot of suggestions on this page and on other questions and sites and nothing works good. One suggestion was to handle errors on web.config inside system.webserver but that just returns blank pages.

    My goal when coming up with this solution was to;

    • NOT REDIRECT
    • Return PROPER STATUS CODES not 200/Ok like the default error handling

    Here is my solution.

    1.Add the following to system.web section

       <system.web>
         <customErrors mode="On" redirectMode="ResponseRewrite">
          <error statusCode="404"  redirect="~/Error/404.aspx" />
          <error statusCode="500" redirect="~/Error/500.aspx" />
         </customErrors>
        <system.web>
    

    The above handles any urls not handled by routes.config and unhandled exceptions especially those encountered on the views. Notice I used aspx not html. This is so I can add a response code on the code behind.

    2. Create a folder called Error (or whatever you prefer) at the root of your project and add the two webforms. Below is my 404 page;

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="404.aspx.cs" Inherits="Myapp.Error._404" %>
    
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title >Page Not found</title>
        <link href="<%=ResolveUrl("~/Content/myapp.css")%>" rel="stylesheet" />
    </head>
    <body>
        <div class="top-nav">
          <a runat="server" class="company-logo" href="~/"></a>
        </div>
        <div>
            <h1>404 - Page Not found</h1>
            <p>The page you are looking for cannot be found.</p>
            <hr />
            <footer></footer>
        </div>
    </body>
    </html>
    

    And on the code behind I set the response code

    protected void Page_Load(object sender, EventArgs e)
    {
        Response.StatusCode = 404;
    }
    

    Do the same for the 500 page

    3.To handle errors within the controllers. There's many ways to do it. This is what worked for me. All my controllers inherit from a base controller. In the base controller, I have the following methods

    protected ActionResult ShowNotFound()
    {
        return ShowNotFound("Page not found....");
    }
    
    protected ActionResult ShowNotFound(string message)
    {
        return ShowCustomError(HttpStatusCode.NotFound, message);
    }
    
    protected ActionResult ShowServerError()
    {
        return ShowServerError("Application error....");
    }
    
    protected ActionResult ShowServerError(string message)
    {
        return ShowCustomError(HttpStatusCode.InternalServerError, message);
    }
    
    protected ActionResult ShowNotAuthorized()
    {
        return ShowNotAuthorized("You are not allowed ....");
    
    }
    
    protected ActionResult ShowNotAuthorized(string message)
    {
        return ShowCustomError(HttpStatusCode.Forbidden, message);
    }
    
    protected ActionResult ShowCustomError(HttpStatusCode statusCode, string message)
    {
        Response.StatusCode = (int)statusCode;
        string title = "";
        switch (statusCode)
        {
            case HttpStatusCode.NotFound:
                title = "404 - Not found";
                break;
            case HttpStatusCode.Forbidden:
                title = "403 - Access Denied";
                break;
            default:
                title = "500 - Application Error";
                break;
        }
        ViewBag.Title = title;
        ViewBag.Message = message;
        return View("CustomError");
    }
    

    4.Add the CustomError.cshtml to your Shared views folder. Below is mine;

    <h1>@ViewBag.Title</h1>
    <br />
    <p>@ViewBag.Message</p>
    

    Now in your application controller you can do something like this;

    public class WidgetsController : ControllerBase
    {
      [HttpGet]
      public ActionResult Edit(int id)
      {
        Try
        {
           var widget = db.getWidgetById(id);
           if(widget == null)
              return ShowNotFound();
              //or return ShowNotFound("Invalid widget!");
           return View(widget);
        }
        catch(Exception ex)
        {
           //log error
           logger.Error(ex)
           return ShowServerError();
        }
      }
    }
    

    Now for the caveat. It won't handle static file errors. So if you have a route such as example.com/widgets and the user changes it to example.com/widgets.html, they will get the IIS default error page so you have to handle IIS level errors some other way.

    0 讨论(0)
提交回复
热议问题