asp.net-mvc länge - Wie kann ich 404 in ASP.NET MVC richtig behandeln?




für meta (17)

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.


Answers

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.


It seems to me that the standard CustomErrors configuration should just work however, due to the reliance on Server.Transfer it seems that the internal implementation of ResponseRewrite isn't compatible with MVC.

This feels like a glaring functionality hole to me, so I decided to re-implement this feature using a HTTP module. The solution below allows you to handle any HTTP status code (including 404) by redirecting to any valid MVC route just as you would do normally.

<customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
    <error statusCode="404" redirect="404.aspx" />
    <error statusCode="500" redirect="~/MVCErrorPage" />
</customErrors>

This has been tested on the following platforms;

  • MVC4 in Integrated Pipeline Mode (IIS Express 8)
  • MVC4 in Classic Mode (VS Development Server, Cassini)
  • MVC4 in Classic Mode (IIS6)

Benefits

  • Generic solution which can be dropped into any MVC project
  • Enables support for traditional custom errors configuration
  • Works in both Integrated Pipeline and Classic modes

The Solution

namespace Foo.Bar.Modules {

    /// <summary>
    /// Enables support for CustomErrors ResponseRewrite mode in MVC.
    /// </summary>
    public class ErrorHandler : IHttpModule {

        private HttpContext HttpContext { get { return HttpContext.Current; } }
        private CustomErrorsSection CustomErrors { get; set; }

        public void Init(HttpApplication application) {
            System.Configuration.Configuration configuration = WebConfigurationManager.OpenWebConfiguration("~");
            CustomErrors = (CustomErrorsSection)configuration.GetSection("system.web/customErrors");

            application.EndRequest += Application_EndRequest;
        }

        protected void Application_EndRequest(object sender, EventArgs e) {

            // only handle rewrite mode, ignore redirect configuration (if it ain't broke don't re-implement it)
            if (CustomErrors.RedirectMode == CustomErrorsRedirectMode.ResponseRewrite && HttpContext.IsCustomErrorEnabled) {

                int statusCode = HttpContext.Response.StatusCode;

                // if this request has thrown an exception then find the real status code
                Exception exception = HttpContext.Error;
                if (exception != null) {
                    // set default error status code for application exceptions
                    statusCode = (int)HttpStatusCode.InternalServerError;
                }

                HttpException httpException = exception as HttpException;
                if (httpException != null) {
                    statusCode = httpException.GetHttpCode();
                }

                if ((HttpStatusCode)statusCode != HttpStatusCode.OK) {

                    Dictionary<int, string> errorPaths = new Dictionary<int, string>();

                    foreach (CustomError error in CustomErrors.Errors) {
                        errorPaths.Add(error.StatusCode, error.Redirect);
                    }

                    // find a custom error path for this status code
                    if (errorPaths.Keys.Contains(statusCode)) {
                        string url = errorPaths[statusCode];

                        // avoid circular redirects
                        if (!HttpContext.Request.Url.AbsolutePath.Equals(VirtualPathUtility.ToAbsolute(url))) {

                            HttpContext.Response.Clear();
                            HttpContext.Response.TrySkipIisCustomErrors = true;

                            HttpContext.Server.ClearError();

                            // do the redirect here
                            if (HttpRuntime.UsingIntegratedPipeline) {
                                HttpContext.Server.TransferRequest(url, true);
                            }
                            else {
                                HttpContext.RewritePath(url, false);

                                IHttpHandler httpHandler = new MvcHttpHandler();
                                httpHandler.ProcessRequest(HttpContext);
                            }

                            // return the original status code to the client
                            // (this won't work in integrated pipleline mode)
                            HttpContext.Response.StatusCode = statusCode;

                        }
                    }

                }

            }

        }

        public void Dispose() {

        }


    }

}

Verwendung

Include this as the final HTTP module in your web.config

  <system.web>
    <httpModules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </httpModules>
  </system.web>

  <!-- IIS7+ -->
  <system.webServer>
    <modules>
      <add name="ErrorHandler" type="Foo.Bar.Modules.ErrorHandler" />
    </modules>
  </system.webServer>

For those of you paying attention you will notice that in Integrated Pipeline mode this will always respond with HTTP 200 due to the way Server.TransferRequest works. To return the proper error code I use the following error controller.

public class ErrorController : Controller {

    public ErrorController() { }

    public ActionResult Index(int id) {
        // pass real error code to client
        HttpContext.Response.StatusCode = id;
        HttpContext.Response.TrySkipIisCustomErrors = true;

        return View("Errors/" + id.ToString());
    }

}

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

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

https://.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.


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

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


ASP.NET MVC unterstützt benutzerdefinierte 404-Seiten nicht sehr gut. Benutzerdefinierte Controller-Factory, Catch-All-Route, Basis-Controller-Klasse mit HandleUnknownAction - argh!

Benutzerdefinierte IIS-Fehlerseiten sind bisher die bessere Alternative:

web.config

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

ErrorController

public class ErrorController : Controller
{
    public ActionResult PageNotFound()
    {
        Response.StatusCode = 404;
        return View();
    }
}

Beispielprojekt


Hier ist eine weitere Methode, die MVC-Tools verwendet, mit denen Sie Anfragen an fehlerhafte Controllernamen, ungültige Routennamen und alle anderen Kriterien bearbeiten können, die in einer Action-Methode passen. Persönlich bevorzuge ich es, so viele Einstellungen von web.config wie möglich zu vermeiden, da sie die 302/200-Umleitung Server.Transfer und ResponseRewrite ( Server.Transfer ) nicht mit Razor-Ansichten unterstützen. Ich würde lieber eine 404 mit einer benutzerdefinierten Fehlerseite aus SEO Gründen zurückgeben.

Einiges davon ist die neue Technik von Cottsak.

Diese Lösung verwendet außerdem minimale web.config-Einstellungen, die stattdessen die MVC 3-Fehlerfilter bevorzugen.

Verwendung

Werfen Sie einfach eine HttpException von einer Aktion oder einem benutzerdefinierten ActionFilterAttribute aus.

Throw New HttpException(HttpStatusCode.NotFound, "[Custom Exception Message Here]")

Schritt 1

Fügen Sie Ihrer web.config die folgende Einstellung hinzu. Dies ist erforderlich, um MVCs HandleErrorAttribute zu verwenden.

<customErrors mode="On" redirectMode="ResponseRedirect" />

Schritt 2

Fügen Sie ein benutzerdefiniertes HandleHttpErrorAttribute ähnlich dem HandleErrorAttribute des MVC-Frameworks hinzu, mit Ausnahme von HTTP-Fehlern:

<AttributeUsage(AttributeTargets.All, AllowMultiple:=True)>
Public Class HandleHttpErrorAttribute
    Inherits FilterAttribute
    Implements IExceptionFilter

    Private Const m_DefaultViewFormat As String = "ErrorHttp{0}"

    Private m_HttpCode As HttpStatusCode
    Private m_Master As String
    Private m_View As String

    Public Property HttpCode As HttpStatusCode
        Get
            If m_HttpCode = 0 Then
                Return HttpStatusCode.NotFound
            End If
            Return m_HttpCode
        End Get
        Set(value As HttpStatusCode)
            m_HttpCode = value
        End Set
    End Property

    Public Property Master As String
        Get
            Return If(m_Master, String.Empty)
        End Get
        Set(value As String)
            m_Master = value
        End Set
    End Property

    Public Property View As String
        Get
            If String.IsNullOrEmpty(m_View) Then
                Return String.Format(m_DefaultViewFormat, Me.HttpCode)
            End If
            Return m_View
        End Get
        Set(value As String)
            m_View = value
        End Set
    End Property

    Public Sub OnException(filterContext As System.Web.Mvc.ExceptionContext) Implements System.Web.Mvc.IExceptionFilter.OnException
        If filterContext Is Nothing Then Throw New ArgumentException("filterContext")

        If filterContext.IsChildAction Then
            Return
        End If

        If filterContext.ExceptionHandled OrElse Not filterContext.HttpContext.IsCustomErrorEnabled Then
            Return
        End If

        Dim ex As HttpException = TryCast(filterContext.Exception, HttpException)
        If ex Is Nothing OrElse ex.GetHttpCode = HttpStatusCode.InternalServerError Then
            Return
        End If

        If ex.GetHttpCode <> Me.HttpCode Then
            Return
        End If

        Dim controllerName As String = filterContext.RouteData.Values("controller")
        Dim actionName As String = filterContext.RouteData.Values("action")
        Dim model As New HandleErrorInfo(filterContext.Exception, controllerName, actionName)

        filterContext.Result = New ViewResult With {
            .ViewName = Me.View,
            .MasterName = Me.Master,
            .ViewData = New ViewDataDictionary(Of HandleErrorInfo)(model),
            .TempData = filterContext.Controller.TempData
        }
        filterContext.ExceptionHandled = True
        filterContext.HttpContext.Response.Clear()
        filterContext.HttpContext.Response.StatusCode = Me.HttpCode
        filterContext.HttpContext.Response.TrySkipIisCustomErrors = True
    End Sub
End Class

Schritt 3

Fügen Sie der GlobalFilterCollection ( GlobalFilters.Filters ) in Global.asax Filter Global.asax . In diesem Beispiel werden alle InternalServerError (500) Views/Shared/Error.vbhtml an die freigegebene Fehleransicht ( Views/Shared/Error.vbhtml ) weitergeleitet. NotFound (404) -Fehler werden auch in den gemeinsamen Ansichten an ErrorHttp404.vbhtml gesendet. Ich habe hier einen Fehler 401 hinzugefügt, um Ihnen zu zeigen, wie dies für zusätzliche HTTP-Fehlercodes erweitert werden kann. Beachten Sie, dass es sich um gemeinsam genutzte Ansichten handeln muss, die alle das System.Web.Mvc.HandleErrorInfo Objekt als Modell verwenden.

filters.Add(New HandleHttpErrorAttribute With {.View = "ErrorHttp401", .HttpCode = HttpStatusCode.Unauthorized})
filters.Add(New HandleHttpErrorAttribute With {.View = "ErrorHttp404", .HttpCode = HttpStatusCode.NotFound})
filters.Add(New HandleErrorAttribute With {.View = "Error"})

Schritt 4

Erstellen Sie eine Basis-Controller-Klasse und erben Sie davon in Ihren Controllern. This step allows us to handle unknown action names and raise the HTTP 404 error to our HandleHttpErrorAttribute.

Public Class BaseController
    Inherits System.Web.Mvc.Controller

    Protected Overrides Sub HandleUnknownAction(actionName As String)
        Me.ActionInvoker.InvokeAction(Me.ControllerContext, "Unknown")
    End Sub

    Public Function Unknown() As ActionResult
        Throw New HttpException(HttpStatusCode.NotFound, "The specified controller or action does not exist.")
        Return New EmptyResult
    End Function
End Class

Step 5

Create a ControllerFactory override, and override it in your Global.asax file in Application_Start. This step allows us to raise the HTTP 404 exception when an invalid controller name has been specified.

Public Class MyControllerFactory
    Inherits DefaultControllerFactory

    Protected Overrides Function GetControllerInstance(requestContext As System.Web.Routing.RequestContext, controllerType As System.Type) As System.Web.Mvc.IController
        Try
            Return MyBase.GetControllerInstance(requestContext, controllerType)
        Catch ex As HttpException
            Return DependencyResolver.Current.GetService(Of BaseController)()
        End Try
    End Function
End Class

'In Global.asax.vb Application_Start:

controllerBuilder.Current.SetControllerFactory(New MyControllerFactory)

Step 6

Include a special route in your RoutTable.Routes for the BaseController Unknown action. This will help us raise a 404 in the case where a user accesses an unknown controller, or unknown action.

'BaseController
routes.MapRoute( _
    "Unknown", "BaseController/{action}/{id}", _
    New With {.controller = "BaseController", .action = "Unknown", .id = UrlParameter.Optional} _
)

Zusammenfassung

This example demonstrated how one can use the MVC framework to return 404 Http Error Codes to the browser without a redirect using filter attributes and shared error views. It also demonstrates showing the same custom error page when invalid controller names and action names are specified.

I'll add a screenshot of an invalid controller name, action name, and a custom 404 raised from the Home/TriggerNotFound action if I get enough votes to post one =). Fiddler returns a 404 message when I access the following URLs using this solution:

/InvalidController
/Home/InvalidRoute
/InvalidController/InvalidRoute
/Home/TriggerNotFound

cottsak's post above and these articles were good references.


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.


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


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

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.


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!


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.


1) Make abstract Controller class.

public abstract class MyController:Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = 404;
        return View("NotFound");
    }

    protected override void HandleUnknownAction(string actionName)
    {
        this.ActionInvoker.InvokeAction(this.ControllerContext, "NotFound");
    }
    protected override void OnAuthorization(AuthorizationContext filterContext) { }
}  

2) Make inheritence from this abstract class in your all controllers

public class HomeController : MyController
{}  

3) And add a view named "NotFound" in you View-Shared folder.


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.


Der beste Weg ist die Verwendung von Global.Asax, da Sie alle Arten von Fehlern verwalten können (Ajax-Aufrufe / alle unerwarteten Fehler). mit anderen kannst du es nicht tun.

So was:

protected void Application_Error()
{
    HttpContext httpContext = HttpContext.Current;
    if (httpContext != null)
    {
        RequestContext requestContext = ((MvcHandler)httpContext.CurrentHandler).RequestContext;
        /* When the request is ajax the system can automatically handle a mistake with a JSON response. 
           Then overwrites the default response */
        if (requestContext.HttpContext.Request.IsAjaxRequest())
        {
            httpContext.Response.Clear();
            string controllerName = requestContext.RouteData.GetRequiredString("controller");
            IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = factory.CreateController(requestContext, controllerName);
            ControllerContext controllerContext = new ControllerContext(requestContext, (ControllerBase)controller);

            JsonResult jsonResult = new JsonResult
            {
                Data = new { success = false, serverError = "500" },
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
            jsonResult.ExecuteResult(controllerContext);
            httpContext.Response.End();
        }
        else
        {
            httpContext.Response.Redirect("~/Error");
        }
    }
}




asp.net-mvc http-status-code-404