Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Error Pages with Layout

I've been fighting to get custom error pages to work and keep finding my way back to simply using a static page. Though the static page works, it would require remaking the navigation bar which we would like to avoid at this time. I'm currently using the following to specify custom error pages.

Asp.net Error Handling

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

IIS Error Handling

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

Is there a method of implementing dynamic custom error pages that can handle for both IIS errors and Asp.net errors?

like image 858
J-Americano Avatar asked Jul 07 '16 14:07

J-Americano


2 Answers

I struggled with this as well and searched for a long time. As far as I can tell there is no way to create a dynamic custom error page that gets served up for IIS Errors for requests that don't enter the .net pipeline. Like you, I ended up having two 404 error pages. One dynamic .aspx one for file not founds errors resulting from requests that entered the .net pipeline, and one .html one for file not found requests that never entered the .net pipeline. The person who downvoted your question probably didn't realize that you are asking a very good, and difficult question, that has no easy solution. I will upvote your question to help compensate.

like image 58
RonC Avatar answered Oct 24 '22 04:10

RonC


I got around the same issue in my ASP.Net MVC projects by piping everything through .Net via handlers.

  <system.webServer>
    <handlers>
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" />
      <remove name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" />
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="OPTIONSVerbHandler" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_32bit" path="*.*" verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" />
      <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*.*" verb="GET,HEAD,POST,DEBUG,PUT,DELETE,PATCH,OPTIONS" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*.*" verb="GET,HEAD,POST,DEBUG,PUT,DELETE" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
  </system.webServer>

First an ErrorController was created to handle request errors and not found requests like.

[AllowAnonymous]
public class ErrorController : Controller {
    // GET: Error
    public ActionResult NotFound() {
        Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
        Response.TrySkipIisCustomErrors = true;
        HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
        HttpContext.Response.TrySkipIisCustomErrors = true;
        return View();
    }

    public ActionResult Error() {
        Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
        Response.TrySkipIisCustomErrors = true;
        return View();
    }
}

You will notice that I call TrySkipIisCustomErrors to try to avoid IIS errors

Then a base controller to handle all unknown actions that would map to the ErrorController.NotFound action was created.

public abstract class FrontOfficeControllerBase : Controller {
    protected override void HandleUnknownAction(string actionName) {
        var data = ViewData;
        //Custom code to resolve the view.
        //ViewResult view = this.View<ErrorController>(c => c.NotFound());

        //Manually create view with view Data
        ViewResult view = new ViewResult();
        view.ViewData = new ViewDataDictionary();
        view.ViewData["controller"] = "Error";
        view.ViewData["action"] = "NotFound";

        if (data != null && data.Count > 0) {
            data.ToList().ForEach(view.ViewData.Add);
        }

        Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
        Response.TrySkipIisCustomErrors = true;
        view.ExecuteResult(this.ControllerContext);
    }
}

All Controllers would inherit from this base controller.

A catch all route was configured after all other routes.

routes.MapRoute(
    name: "404-NotFound",
    url: "NotFound",
    defaults: new { controller = "Error", action = "NotFound" }
);

routes.MapRoute(
    name: "500-Error",
    url: "Error",
    defaults: new { controller = "Error", action = "Error" }
);

routes.MapRoute(
    name: "CatchAll",
    url: "{*any}",
    defaults: new { controller = "Error", action = "NotFound" });

This made sure that if a route was not matched to any of my controllers it would safely route to the ErrorController.NotFound action.

For the views, I created the respective NotFound.shtml and Error.cshtml paged in the Views/Shared folder and they benefited from access to the root layout, which is what I think you were looking for.

In the end I was able to remove both the customErrors and httpErrors from web.config as there was no longer any need for them as every request was managed by the handlers and routed accordingly.

The original idea for this structure came from this article where I mixed and matched the available options till I found a solution that worked/suited my needs.

Exception handling in ASP.NET MVC (6 methods explained)

Hope this helps.

like image 32
Nkosi Avatar answered Oct 24 '22 04:10

Nkosi