asp.net-mvc - titel - title tag länge




Wie kann ich 404 in ASP.NET MVC richtig behandeln? (13)

Ich beginne gerade auf ASP.NET MVC, also ertragen Sie mit mir. Ich habe rund um diese Seite und verschiedene andere gesucht und habe ein paar Implementierungen davon gesehen.

EDIT: Ich habe vergessen zu erwähnen, dass ich RC2 benutze

URL-Routing verwenden:

routes.MapRoute(
    "Error",
     "{*url}",
     new { controller = "Errors", action = "NotFound" }  // 404s
);

Das obige scheint sich um Anfragen wie diese zu kümmern (unter Annahme von Standard-Routentabellen, die durch das anfängliche MVC-Projekt eingerichtet wurden): "/ blah / blah / blah / blah"

Überschreibt HandleUnknownAction () im Controller selbst:

// 404s - handle here (bad action requested
protected override void HandleUnknownAction(string actionName) {
    ViewData["actionName"] = actionName;
    View("NotFound").ExecuteResult(this.ControllerContext);
}  

Die vorherigen Strategien verarbeiten jedoch keine Anforderung an einen fehlerhaften / unbekannten Controller. Zum Beispiel habe ich kein "/ IDoNotExist", wenn ich dies anfordere, bekomme ich die generische 404-Seite vom Webserver und nicht meine 404, wenn ich routing + override verwende.

Meine Frage ist also: Gibt es eine Möglichkeit, diese Art von Anfrage über eine Route oder etwas anderes im MVC-Framework selbst zu erfassen?

ODER sollte ich einfach Web.Config customErrors als mein 404-Handler verwenden und alles vergessen? Ich nehme an, wenn ich mit customErrors gehe, muss ich die generische 404-Seite außerhalb von / Ansichten aufgrund der Web.Config-Einschränkungen für den direkten Zugriff speichern. Jedenfalls wird jede Best Practice oder Anleitung geschätzt.


Anforderungen für 404

Das Folgende sind meine Anforderungen für eine 404-Lösung und unten zeige ich, wie ich es implementiere:

  • Ich möchte angepasste Routen mit schlechten Aktionen behandeln
  • Ich möchte angepasste Routen mit fehlerhaften Controllern behandeln
  • Ich möchte mit nicht übereinstimmenden Routen umgehen (beliebige URLs, die meine App nicht verstehen kann) - ich möchte nicht, dass diese auf Global.asax oder IIS aufblubbern, weil ich dann nicht richtig in meine MVC-App umleiten kann
  • Ich möchte einen Weg, wie oben beschrieben, benutzerdefinierte 404s zu behandeln - wie wenn eine ID für ein Objekt eingereicht wird, das nicht existiert (vielleicht gelöscht)
  • Ich möchte, dass alle meine 404s eine MVC-Ansicht (keine statische Seite) zurückgeben, auf die ich später mehr Daten pumpen kann ( codinghorror.com/blog/2007/03/… ), und sie müssen den HTTP 404-Statuscode zurückgeben

Lösung

Ich denke, Sie sollten Application_Error in der Global.asax für höhere Dinge, wie unbehandelte Ausnahmen und Protokollierung speichern (wie Shay Jacobys Antwort zeigt), aber nicht 404 Handhabung. Das ist der Grund, warum mein Vorschlag die 404-Dateien aus der Global.asax-Datei heraushält.

Schritt 1: Haben Sie einen gemeinsamen Platz für 404-Fehler-Logik

Dies ist eine gute Idee für die Wartbarkeit. Verwenden Sie einen ErrorController damit zukünftige Verbesserungen an Ihrer codinghorror.com/blog/2007/03/… problemlos ErrorController werden können. Stellen Sie außerdem sicher, dass Ihre Antwort den 404-Code enthält !

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
}

Schritt 2: Verwenden Sie eine Basis-Controller-Klasse, so dass Sie einfach Ihre benutzerdefinierte 404-Aktion aufrufen und HandleUnknownAction

404 in ASP.NET MVC müssen an mehreren Stellen abgefangen werden. Die erste ist HandleUnknownAction .

Die InvokeHttp404 Methode erstellt einen gemeinsamen Ort für das ErrorController an den ErrorController und unsere neue Http404 Aktion. Denken Sie 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
}

Schritt 3: Verwenden Sie Dependency Injection in Ihrer Controller Factory und verdrahten 404 HttpExceptions

So (es muss nicht StructureMap sein):

MVC1.0 Beispiel:

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 Beispiel:

    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;
    }

Ich denke, es ist besser, Fehler näher am Ursprung zu finden. Dies ist der Grund, warum ich das obige dem Application_Error Handler vorziehe.

Dies ist der zweite Ort, um 404s zu fangen.

Schritt 4: Fügen Sie Global.asax eine NotFound-Route für URLs hinzu, die nicht in Ihre App geparst werden können

Diese Route sollte auf unsere Aktion Http404 . Beachten Sie, dass der url Parameter eine relative URL ist, weil die Routing-Engine hier den Domain-Teil entfernt. Aus diesem Grund haben wir in Schritt 1 die gesamte bedingte URL-Logik.

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

Dies ist der dritte und letzte Ort, an dem Sie 404 in einer MVC-App finden, die Sie nicht selbst aufrufen. Wenn Sie hier keine unübertroffenen Routen finden, wird MVC das Problem an ASP.NET (Global.asax) weitergeben und das möchten Sie in dieser Situation nicht wirklich.

Schritt 5: Abschließend rufen Sie 404s auf, wenn Ihre App etwas nicht finden kann

Wie wenn eine schlechte ID an meinen Darlehens-Controller übermittelt wird (abgeleitet von 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);
    }

Es wäre schön, wenn all dies an weniger Stellen mit weniger Code verbunden wäre, aber ich denke, dass diese Lösung wartungsfreundlicher, testbarer und ziemlich pragmatisch ist.

Danke für das Feedback bis jetzt. Ich würde gerne mehr bekommen.

HINWEIS: Dies wurde erheblich von meiner ursprünglichen Antwort bearbeitet, aber der Zweck / Anforderungen sind die gleichen - deshalb habe ich keine neue Antwort hinzugefügt


Schnelle Antwort / TL; DR

Für die faulen Leute da draußen:

Install-Package MagicalUnicornMvcErrorToolkit -Version 1.0

Dann entferne diese Zeile von global.asax

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

Und das ist nur für IIS7 + und IIS Express.

Wenn du Cassini benutzt .. naja .. um .. äh .. umständlich ...

Lange, erklärte Antwort

Ich weiß, das wurde beantwortet. Aber die Antwort ist wirklich einfach (Prost an David Fowler und Damian Edwards für die Beantwortung dieser Frage).

Es gibt keine Notwendigkeit, etwas Brauchbares zu tun .

Für ASP.NET MVC3 sind alle Teile vorhanden.

Schritt 1 -> Aktualisieren Sie Ihre web.config in zwei Punkten.

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

und

<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>

Beachten Sie jetzt die ROUTEN, die ich gewählt habe. Du kannst alles verwenden, aber meine Routen sind

  • /NotFound <- für eine 404 nicht gefunden, Fehlerseite.
  • /ServerError <- Für jeden anderen Fehler, /ServerError Fehler ein, die in meinem Code vorkommen. Dies ist ein 500 Interner Serverfehler

Sehen Sie, wie der erste Abschnitt in <system.web> nur einen benutzerdefinierten Eintrag hat? Der statusCode="404" Eintrag? Ich habe nur einen Status-Code aufgelistet, weil alle anderen Fehler, einschließlich der 500 Server Error (dh diese lästigen Fehler, die passiert, wenn Ihr Code einen Fehler hat und stürzt die Anfrage des Benutzers). Alle anderen Fehler werden von der Einstellung defaultRedirect="/ServerError" .. welches besagt, wenn Sie keine 404 Seite nicht gefunden haben, dann gehen Sie bitte zur Route /ServerError .

OK. Das ist aus dem Weg .. jetzt zu meinen Routen in global.asax

Schritt 2 - Erstellen der Routen in Global.asax

Hier ist meine vollständige Streckenabschnitt ..

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}
        );
}

Das listet zwei ignorieren Routen -> axd's und favicons (ooo! Bonus ignorieren Route, für Sie!) Dann (und die Reihenfolge ist hier IMPERATIV), habe ich meine zwei explizite Fehlerbehandlung Routen .. gefolgt von anderen Routen. In diesem Fall der Standardwert. Natürlich habe ich mehr, aber das ist etwas Besonderes auf meiner Website. Stellen Sie nur sicher, dass die Fehlerrouten an der Spitze der Liste stehen. Bestellung ist zwingend erforderlich .

Schließlich, während wir uns in unserer global.asax Datei befinden, registrieren wir das HandleError-Attribut NICHT global. Nein, nein, nein, mein Herr. Nadda. Nee. Nien. Negativ. Neinoooooooo ...

Entfernen Sie diese Zeile aus global.asax

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

Schritt 3 - Erstellen Sie den Controller mit den Aktionsmethoden

Jetzt ... fügen wir einen Controller mit zwei Aktionsmethoden hinzu ...

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, lass uns das überprüfen. Zuallererst gibt es hier [HandleError] Attribut [HandleError] . Warum? Weil das eingebaute ASP.NET Framework bereits Fehler verarbeitet UND wir haben die ganze Scheiße spezifiziert, die wir tun müssen, um einen Fehler zu behandeln :) Es ist in dieser Methode!

Als nächstes habe ich die zwei Aktionsmethoden. Nichts Hartes dort. Wenn Sie irgendwelche Ausnahmeinformationen Server.GetLastError() möchten, können Sie Server.GetLastError() , um diese Informationen zu erhalten.

Bonus WTF: Ja, ich habe eine dritte Aktionsmethode gemacht, um die Fehlerbehandlung zu testen.

Schritt 4 - Erstellen Sie die Ansichten

Und schließlich, erstellen Sie zwei Ansichten. Setzen Sie sie in den normalen Ansichtspunkt für diesen Controller.

Bonuskommentare

  • Sie benötigen keinen Application_Error(object sender, EventArgs e)
  • Die obigen Schritte funktionieren alle 100% perfekt mit Elmah . Elmah Fraking wroxs!

Und das, meine Freunde, sollte es sein.

Nun, Glückwunsch, dass Sie so viel gelesen haben und ein Einhorn als Preis haben!


Der Code stammt von http://blogs.microsoft.co.il/blogs/shay/archive/2009/03/06/real-world-error-hadnling-in-asp-net-mvc-rc2.aspx und funktioniert in ASP.net MVC 1.0 auch

So behandle ich http-Ausnahmen:

protected void Application_Error(object sender, EventArgs e)
{
   Exception exception = Server.GetLastError();
   // Log the exception.

   ILogger logger = Container.Resolve<ILogger>();
   logger.Error(exception);

   Response.Clear();

   HttpException httpException = exception as HttpException;

   RouteData routeData = new RouteData();
   routeData.Values.Add("controller", "Error");

   if (httpException == null)
   {
       routeData.Values.Add("action", "Index");
   }
   else //It's an Http Exception, Let's handle it.
   {
       switch (httpException.GetHttpCode())
       {
          case 404:
              // Page not found.
              routeData.Values.Add("action", "HttpError404");
              break;
          case 500:
              // Server error.
              routeData.Values.Add("action", "HttpError500");
              break;

           // Here you can handle Views to other error codes.
           // I choose a General error template  
           default:
              routeData.Values.Add("action", "General");
              break;
      }
  }           

  // Pass exception details to the target error View.
  routeData.Values.Add("error", exception);

  // Clear the error on server.
  Server.ClearError();

  // Avoid IIS7 getting in the middle
  Response.TrySkipIisCustomErrors = true; 

  // Call target Controller and pass the routeData.
  IController errorController = new ErrorController();
  errorController.Execute(new RequestContext(    
       new HttpContextWrapper(Context), routeData));
}

Der einzige Weg, wie ich @cottsaks Methode für ungültige Controller bekommen konnte, war, die existierende Route-Anfrage in der CustomControllerFactory wie folgt zu modifizieren:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        try
        {
            if (controllerType == null)
                return base.GetControllerInstance(requestContext, controllerType); 
            else
                return ObjectFactory.GetInstance(controllerType) as Controller;
        }
        catch (HttpException ex)
        {
            if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound)
            {
                requestContext.RouteData.Values["controller"] = "Error";
                requestContext.RouteData.Values["action"] = "Http404";
                requestContext.RouteData.Values.Add("url", requestContext.HttpContext.Request.Url.OriginalString);

                return ObjectFactory.GetInstance<ErrorController>();
            }
            else
                throw ex;
        }
    }
}

Ich sollte erwähnen, dass ich MVC 2.0 verwende.


Ich habe A LOT untersucht, wie man 404 in MVC (speziell MVC3) richtig verwaltet, und das ist IMHO die beste Lösung, die ich mir ausgedacht habe:

In global.asax:

public class MvcApplication : HttpApplication
{
    protected void Application_EndRequest()
    {
        if (Context.Response.StatusCode == 404)
        {
            Response.Clear();

            var rd = new RouteData();
            rd.DataTokens["area"] = "AreaName"; // In case controller is in another area
            rd.Values["controller"] = "Errors";
            rd.Values["action"] = "NotFound";

            IController c = new ErrorsController();
            c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
        }
    }
}

FehlerController:

public sealed class ErrorsController : Controller
{
    public ActionResult NotFound()
    {
        ActionResult result;

        object model = Request.Url.PathAndQuery;

        if (!Request.IsAjaxRequest())
            result = View(model);
        else
            result = PartialView("_NotFound", model);

        return result;
    }
}

(Wahlweise)

Erläuterung:

AFAIK, gibt es 6 verschiedene Fälle, die eine ASP.NET MVC3-Anwendungen 404s erzeugen können.

(Wird automatisch von ASP.NET Framework generiert :)

(1) Eine URL findet keine Übereinstimmung in der Routentabelle.

(Automatisch generiert von ASP.NET MVC Framework :)

(2) Eine URL findet eine Übereinstimmung in der Routentabelle, spezifiziert jedoch einen nicht existierenden Controller.

(3) Eine URL findet eine Übereinstimmung in der Routentabelle, spezifiziert jedoch eine nicht existierende Aktion.

(Manuell generiert :)

(4) Eine Aktion gibt ein HttpNotFoundResult mit der Methode HttpNotFound () zurück.

(5) Eine Aktion löst eine HttpException mit dem Statuscode 404 aus.

(6) Eine Aktion ändert die Response.StatusCode-Eigenschaft manuell in 404.

Normalerweise möchten Sie 3 Ziele erreichen:

(1) Zeigen Sie dem Benutzer eine benutzerdefinierte 404-Fehlerseite an.

(2) Pflegen Sie den 404-Status-Code auf der Client-Antwort (besonders wichtig für SEO).

(3) Senden Sie die Antwort direkt, ohne eine 302-Umleitung einzubeziehen.

Es gibt verschiedene Möglichkeiten, dies zu erreichen:

(1)

<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>

Probleme mit dieser Lösung:

  1. Erfüllt nicht das Ziel (1) in den Fällen (1), (4), (6).
  2. Erfüllt Objektiv (2) nicht automatisch. Es muss manuell programmiert werden.
  3. Entspricht nicht dem Ziel (3).

(2)

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Probleme mit dieser Lösung:

  1. Funktioniert nur mit IIS 7+.
  2. Erfüllt in den Fällen (2), (3), (5) nicht das Ziel (1).
  3. Erfüllt Objektiv (2) nicht automatisch. Es muss manuell programmiert werden.

(3)

<system.webServer>
    <httpErrors errorMode="Custom" existingResponse="Replace">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Probleme mit dieser Lösung:

  1. Funktioniert nur mit IIS 7+.
  2. Erfüllt Objektiv (2) nicht automatisch. Es muss manuell programmiert werden.
  3. Es verdeckt Anwendungs-HTTP-Ausnahmen. ZB kann customErrors, System.Web.Mvc.HandleErrorAttribute usw. nicht benutzt werden. Es kann nicht nur allgemeine Fehlerseiten zeigen.

(4)

<system.web>
    <customErrors mode="On">
        <error statusCode="404" redirect="~/Errors/NotFound"/>
    </customError>
</system.web>

und

<system.webServer>
    <httpErrors errorMode="Custom">
        <remove statusCode="404"/>
        <error statusCode="404" path="App/Errors/NotFound" responseMode="ExecuteURL"/>
    </httpErrors>
</system.webServer>

Probleme mit dieser Lösung:

  1. Funktioniert nur mit IIS 7+.
  2. Erfüllt Objektiv (2) nicht automatisch. Es muss manuell programmiert werden.
  3. Entspricht in den Fällen (2), (3), (5) nicht dem Ziel (3).

Leute, die damit schon einmal Probleme hatten, versuchten sogar, eigene Bibliotheken zu erstellen (siehe http://aboutcode.net/2011/02/26/handling-not-found-with-asp-net-mvc3.html ). Aber die vorherige Lösung scheint alle Fälle abzudecken, ohne die Komplexität der Verwendung einer externen Bibliothek.


Ich mag wirklich Cottsaks Lösung und denke, es ist sehr klar erklärt. Mein einziger Zusatz war, Schritt 2 wie folgt zu ändern

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
}

Im Grunde stoppt dies URLs, die ungültige Aktionen enthalten, UND Controller lösen die Ausnahmeroutine zweimal aus. zB für URLs wie asdfsdf / dfgdfgd


Adding my solution, which is almost identical to Herman Kan's, with a small wrinkle to allow it to work for my project.

Create a custom error controller:

public class Error404Controller : BaseController
{
    [HttpGet]
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View("404");
    }
}

Then create a custom controller factory:

public class CustomControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        return controllerType == null ? new Error404Controller() : base.GetControllerInstance(requestContext, controllerType);
    }
}

Finally, add an override to the custom error controller:

protected override void HandleUnknownAction(string actionName)
{
    var errorRoute = new RouteData();
    errorRoute.Values.Add("controller", "Error404");
    errorRoute.Values.Add("action", "PageNotFound");
    new Error404Controller().Execute(new RequestContext(HttpContext, errorRoute));
}

Und das ist es. No need for Web.config changes.


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

Hier ist meine Lösung.

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. Das hat bei mir funktioniert. 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.


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. (ie 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://.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://.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.


In MVC4 WebAPI 404 can be handle in the following way,

COURSES APICONTROLLER

    // GET /api/courses/5
    public HttpResponseMessage<Courses> Get(int id)
    {
        HttpResponseMessage<Courses> resp = null;

        var aCourse = _courses.Where(c => c.Id == id).FirstOrDefault();

        resp = aCourse == null ? new HttpResponseMessage<Courses>(System.Net.HttpStatusCode.NotFound) : new HttpResponseMessage<Courses>(aCourse);

        return resp;
    }

HOME CONTROLLER

public ActionResult Course(int id)
{
    return View(id);
}

VIEW

<div id="course"></div>
<script type="text/javascript">
    var id = @Model;
    var course = $('#course');
    $.ajax({    
        url: '/api/courses/' + id,
        success: function (data) {
            course.text(data.Name);
        },
        statusCode: {
            404: function() 
            {
                course.text('Course not available!');    
            }
        }
    });
</script>

GLOBAL

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );

    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

RESULTS


My shortened solution that works with unhandled areas, controllers and actions:

  1. Create a view 404.cshtml.

  2. Create a base class for your controllers:

    public class Controller : System.Web.Mvc.Controller
    {
        protected override void HandleUnknownAction(string actionName)
        {
            Http404().ExecuteResult(ControllerContext);
        }
    
        protected virtual ViewResult Http404()
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return View("404");
        }
    }
    
  3. Create a custom controller factory returning your base controller as a fallback:

    public class ControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            if (controllerType != null)
                return base.GetControllerInstance(requestContext, controllerType);
    
            return new Controller();
        }
    }
    
  4. Add to Application_Start() the following line:

    ControllerBuilder.Current.SetControllerFactory(typeof(ControllerFactory));
    

My solution, in case someone finds it useful.

In Web.config:

<system.web>
    <customErrors mode="On" defaultRedirect="Error" >
      <error statusCode="404" redirect="~/Error/PageNotFound"/>
    </customErrors>
    ...
</system.web>

In Controllers/ErrorController.cs :

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        if(Request.IsAjaxRequest()) {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            return Content("Not Found", "text/plain");
        }

        return View();
    }
}

Add a PageNotFound.cshtml in the Shared folder, and that's it.


Try NotFoundMVC on nuget. It works , no setup.





http-status-code-404