Custom 404 page in EPi 7 MVC

Marija Jemuovic
Member since: 2010
 

I want to have a 404 page, that is an EPiServer page.

This is my web.config

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

This is my Controller:

    public class NotFoundPageController : PageController<NotFoundPage>
    {
        public ActionResult Index(NotFoundPage currentPage)
        {
            var viewModel = new NotFoundPageViewModel();
            Mapper.Map(currentPage, viewModel);

            Response.StatusCode = 404;

            return View(viewModel);
        }
    }

    and finally, my Global.asax part:

        protected void Application_Error(object sender, EventArgs e)
        {
            var lastError = HttpContext.Current.Server.GetLastError();
            if (lastError != null)
            {
                var httpException = lastError as HttpException;
                if (httpException != null && httpException.GetHttpCode() == 404)
                {
                    RedirectTo404ErrorPage();
                }
            }
        }

        private static void RedirectTo404ErrorPage()
        {
            var startPage = ContentReference.StartPage.GetPage() as StartPage;
            if (startPage != null && startPage.ErrorPage != null)
            {
                var errorPage = startPage.ErrorPage.GetPage();
                if (errorPage != null)
                {
                    HttpContext.Current.Response.Redirect(errorPage.ToFriendlyURL());
                }
            }
        }

    

What I saw online is that MVC should have a different 404 error handling, such as this: http://stackoverflow.com/questions/619895/how-can-i-properly-handle-404-in-asp-net-mvc, however, it's related to non-CMS MVC.

My code gives me indefinite redirection and if I ommit Response.StatusCode = 404; than I don't get the 404 status code in the browser.

Has anyone got ahead of this?

 

#67838 Mar 13, 2013 14:46
  • Mari Jørgensen
    Member since: 2003
     

    To avoid possible infinite redirection issues I'm using static html files for 404 and 500:

    <httpErrors errorMode="Custom" defaultResponseMode="File" existingResponse="Replace">
    <clear />
    <error statusCode="404" path="Static\404.html" />
    <error statusCode="500" path="Static\Error.html" />
    </httpErrors>

    #68170 Mar 18, 2013 10:50
  •  

    I'm also looking for a solution to this problem. Of course I could use static html files as Mari suggests, but that's not really a "solution".

    So, anyone got a solution for this?

    #69756 Apr 04, 2013 17:18
  • Danny WINBOURNE
    Member since: 2010
     

    I have this in my config:

     <httpErrors errorMode="Custom" existingResponse="Replace">
          <clear/>
          <error statusCode="404" responseMode="ExecuteURL" prefixLanguageFilePath="en" path="/system/notfound/" />
          <error statusCode="500" responseMode="ExecuteURL" prefixLanguageFilePath="en" path="/system/error/" />
        </httpErrors>

        

    Then I just created the pages in the CMS as any other page (so, my path is /system/nofound/)

     

    #69774 Apr 05, 2013 13:14
  • Marija Jemuovic
    Member since: 2010
     

    So, you don't have anything in Global.asax? Do you get the 404 status code for a non-existant page in Fiddler?

    #69776 Apr 05, 2013 13:36
  • Danny WINBOURNE
    Member since: 2010
     

    I added nothing to my Global.asax page.

    It wasn't initally returning a 404 status code, but I just added the following to the top of the templates .cshtml page

    Response.StatusCode = 404;

       

    I have a standard template, so just used logic on the page title in the CMS to dertmine if I should include the status code. 

    #69780 Apr 05, 2013 14:26
  •  

    I thought I have tried Danny's solution yesterday, I think I'll have to give it one more try.

    Anyway, I think I've come up with another working solution. But I still need to test it a bit more, but so far it seems to be working fine. I get 404 status code, it shows information from a epi page, it doesn't redirect and it doesn't get stuck in a redirection loop.

    I'm gonna test it a bit more before I post it here, but in short:

    * Add a routing rule for the path "404"  in global.asax to a controller and a NotFound-action in that controller.
    * Add httpErrors in web.config with a 404 error rule to redirect to "/404" (as in Dannys solution)
    * Write logic in the NotFound-action to get a ContentReference to the epi 404-page and return it as a model to the view.

    #69781 Apr 05, 2013 14:35
  • Marija Jemuovic
    Member since: 2010
     

    I don't have time to try this now, but I think if you just add

    Response.StatusCode = 404;

    to the cshtml, you would get a 404 for the page /404 and you need to get a 404 status code for the page that doesn't exist. If a page doesn't exist, you would want a search engine to get 404 for it and not 301 or 302 or 200.

    #69788 Edited, Apr 05, 2013 15:59
  • Danny WINBOURNE
    Member since: 2010
     

    You know, I think you're probably right thinking about it.

    I'll do some more testing on it, to be sure.

    :|

    #69789 Apr 05, 2013 16:01
  •  

    I'm pretty confident my solution works so here is some sample code,

    In global.asax

            protected override void RegisterRoutes(System.Web.Routing.RouteCollection routes)
            {
    
                    // Route to handle 404's
                    routes.MapRoute(
                        "404",
                        "404",
                        new {controller = "DefaultPage", action = "NotFound"});
    
                base.RegisterRoutes(routes);
            }

        

    In web.config

        <httpErrors errorMode="Custom" existingResponse="Replace">
          <clear/>
          <error statusCode="404" responseMode="ExecuteURL" prefixLanguageFilePath="en" path="/404" />
        </httpErrors>

        

    In DefaultPageController.cs

            public ActionResult NotFound(SitePageData currentPage)
            {
                // Get StartPage
                var startPage = ContentReference.StartPage.GetPage<StartPage>();
    
                // Get ErrorPage
                var notFoundPage = startPage.NotFoundPage.GetPage<ErrorPage>();
    
                if (notFoundPage == null)
                    throw new NullReferenceException("No 404 page has been set on the StartPage");
    
                var model = CreateModel(notFoundPage);
    
                // Set RoutingConstants.NodeKey so EPi's extension method RequestContext.GetContentLink() will work
                ControllerContext.RouteData.DataTokens[RoutingConstants.NodeKey] = startPage.NotFoundPage;
    
                Response.StatusCode = 404;
                return View("~/Views/ErrorPage/Index.cshtml", model);
            }

     And finally add a property on the StartPage-model,

    public virtual PageReference NotFoundPage { get; set; }

           

    As I said I'm pretty sure this code works, but it's more a proof of concept and is not production ready code!

    EDIT:

    Some of the code is using extension methods and methods that's part of my solution, but I'll hope you'll get the idea by looking at the code. Feel free to let me know if anything is unclear. 

    #69797 Edited, Apr 05, 2013 19:17
  • DavidRutqvist
    Member since: 2013
     

    I tested Daniel's solution and it worked like a charm!

    I made my controller like this:

            public ActionResult NotFound(SitePageData currentPage)
            {
                StartPage startPage = (StartPage)DataFactory.Instance.GetPage(PageReference.StartPage);
                var errorPage = DataFactory.Instance.GetPage(startPage.NotFoundPage);
    
                // Set RoutingConstants.NodeKey so EPi's extension method RequestContext.GetContentLink() will work
                ControllerContext.RouteData.DataTokens[RoutingConstants.NodeKey] = startPage.NotFoundPage;
    
                Response.StatusCode = 404;
    
                return RedirectToAction("Index");
            }

    That will send me to my specified EPiServer page. For example if i choose my About page I will be sent to www.domain.com/About . This also works with ViewModels and such.

    #70613 Apr 24, 2013 12:44
  •  

    David, nice! :-)

    #70614 Apr 24, 2013 12:51
  •  

    David and Daniel, Thank you so much for your solution. A combination of the two has worked perfectly for me, just what I was looking for. 

    Thank you!!

    #73491 Jul 24, 2013 13:10
  •  

    Dave, glad you found it useful! :-)

    #73585 Jul 30, 2013 14:39
  • Danny WINBOURNE
    Member since: 2010
     

    Daniel, just trying to implement your soltuon.

    I can't seem to get your example working. Is "CreateModel" a custom method you have? 

    Edit: Sorry, spotted you mention your extension methods.. I'll try and work out what's to do.

    #73592 Edited, Jul 31, 2013 10:42
  •  

    Danny, yeah as you already noticed that's my method. It's actually code from Alloy MVC exampl,

    /// <summary>
            /// Creates a PageViewModel where the type parameter is the type of the page.
            /// </summary>
            /// <remarks>
            /// Used to create models of a specific type without the calling method having to know that type.
            /// </remarks>
            private static IPageViewModel<T> CreateModel<T>(T page) where T : SitePageData
            {
                var type = typeof(PageViewModel<>).MakeGenericType(page.GetOriginalType());
                return Activator.CreateInstance(type, page) as IPageViewModel<T>;
            }

        

    #73597 Jul 31, 2013 11:50
  • Danny WINBOURNE
    Member since: 2010
     

    I have a working implentation, but on my staging server, it takes 1-2 minutes to display the not found page when requesting a page that doesn't exist.

    On my local build, it loads instantly.

    Any one got any ideas what could be causing this to happen?

    #73603 Jul 31, 2013 15:50
  • Marija Jemuovic
    Member since: 2010
     

    Can you compare web.config files for differences? Perhaps you are missing <clear /> in <httpErrors>?

    Also, check if more redirections occur with Fiddler than only one, perhaps it'll tell you what's going on.

    #73606 Jul 31, 2013 15:57
  •  

    Strange, I haven't hade problems witha request taking minutes to load. Another way to debug things like this is using Glimpse, but I haven't tested Glimpse whit EpiServer so I'm not sure it'll work.

    But start with Fiddler, just like Marija suggested.

    #73607 Jul 31, 2013 16:02
  • Danny WINBOURNE
    Member since: 2010
     

    Cheers for the quick response guys.
    So, using fidler doesn't seem to show up any further redirects. It eventaully completes, and shows as 404, but this takes several minutes.

    Just hitting the /404 page loads right away, with fiddler correctly showing the header s 404.


    Not used glimpse before (although I've seen a demo of it), May take a look and see if it can help, although not sure what I'll be looking for,

    #73609 Jul 31, 2013 16:10
  •  

    Very strange, errmm.. I'm not quite sure what to look for either. 

    Have you disabled debug and compiled in release mode?

    #73610 Jul 31, 2013 16:16
  • Danny WINBOURNE
    Member since: 2010
     

    yep, debug disabled, and compiled in release mode..

    All very strange.. Guess I'll have to open a support ticket.

    #73612 Edited, Jul 31, 2013 16:38
  • Björn Sållarp
    Member since: 2006
     

    Hey Danny,

    Did you solve the problem with the slow error page? I have the same problem right now when running the site in stage. On our dev machines it works as expected (fast).

     

    #78931 Dec 05, 2013 10:37
  • Danny WINBOURNE
    Member since: 2010
     

    Hi Björn,

    Yes, I did solve it. Strangely, all I had to do is install the URL Rewrite module..
    See http://weblogs.asp.net/scottgu/archive/2010/04/20/tip-trick-fix-common-seo-problems-using-the-url-rewrite-extension.aspx for details.. Not sure why it solved it, but it did!

    Good luck

    Danny

    #78932 Dec 05, 2013 10:40
  • Björn Sållarp
    Member since: 2006
     

    Thanks for a fast reply. Unfortunately, that didn't solve it for me. Think you did any other changes on the server or enabled something when you installed URL Rewrite?

    #78934 Dec 05, 2013 11:07
  • Björn Sållarp
    Member since: 2006
     

    Very weird, this might actually be a bug in IIS? When the request is made locally (IsLocal on Request), it's fast. The stage environment is fast if browsed locally. I get the same error on my dev machine when making requests from a remote computer. Seems application_error is hit and then nothing happens for 1-2 minutes before the configured path is hit.

    I ended up rolling a somewhat different solution. Configured PassThrough on existingResponse and made a module that handle 404 and errors. Works fine.

    #78954 Dec 05, 2013 15:19
  •  

    I had the same issue, the problem is the route mapping:

    protected override void RegisterRoutes(RouteCollection routes)
     {
            routes.MapRoute("404", "404", 
                new { controller = "ContentPage", action = "NotFound" });
    
            base.RegisterRoutes(routes);
     }

     For some reason the 404 page is called in an infinite loop till the execution timeout kicks in. I removed that route and added a catch all route (just in case, I think it isn't neccesary):

    protected override void RegisterRoutes(RouteCollection routes)
    {
        base.RegisterRoutes(routes);
    
        routes.MapRoute("Error", "{*url}",
            new { controller = "ContentPage", action = "NotFound" });
    }

    And now it works fast and great

    #81619 Edited, Feb 20, 2014 16:36
  •  

    Erwin G,

     

    Strange! That shouldn't be needed. We're not seeing that problem on any of our ~10 sites. But thanks for the heads up! :)

    #81630 Feb 21, 2014 9:01
  • David Tellander
    Member since: 2008
     

    Another thing to check regarding the timeout-issue on remote calls to the site is to disable EPiServer's own global error handling in episerver.config. It is set to 'RemoteOnly' by default. Set it to 'Off'.

    
      
    
    


    This solved it for us.

    /David

    #91173 Sep 29, 2014 10:02
  • Mark Bagnall
    Member since: 2004
     

    I've added the suggested code to an Alloy site. This is my NotFound method in the controller:

    public ActionResult NotFound(SitePageData currentPage)
    {
        StartPage startPage = (StartPage)DataFactory.Instance.GetPage(PageReference.StartPage);
        var errorPage = DataFactory.Instance.GetPage(startPage.NotFoundPage) as SitePageData;
     
        var model = CreateModel(errorPage);
    
        // Set RoutingConstants.NodeKey so EPi's extension method RequestContext.GetContentLink() will work
        ControllerContext.RouteData.DataTokens[RoutingConstants.NodeKey] = startPage.NotFoundPage;
    
        Response.StatusCode = 404;
     
        return View("~/Views/StandardPage/Index.cshtml", model);
    }
    

    I get the following error in PageViewContextFactory.cs:

    The provided content link does not have a value.
    Parameter name: contentLink


    Line 55:         public virtual IContent GetSection(ContentReference contentLink)
    Line 56:         {
    Line 57: var currentContent = _contentLoader.Get<IContent>(contentLink);
    Line 58:             if(currentContent.ParentLink != null && currentContent.ParentLink.CompareToIgnoreWorkID(ContentReference.StartPage))
    Line 59:             {

    I'm obviously missing something, any ideas?

    #111667 Oct 17, 2014 11:06
  • Mark Bagnall
    Member since: 2004
     

    I've added the suggested code to an Alloy site. This is my NotFound method in the controller:

    public ActionResult NotFound(SitePageData currentPage)
    {
        StartPage startPage = (StartPage)DataFactory.Instance.GetPage(PageReference.StartPage);
        var errorPage = DataFactory.Instance.GetPage(startPage.NotFoundPage) as SitePageData;
     
        var model = CreateModel(errorPage);
    
        // Set RoutingConstants.NodeKey so EPi's extension method RequestContext.GetContentLink() will work
        ControllerContext.RouteData.DataTokens[RoutingConstants.NodeKey] = startPage.NotFoundPage;
    
        Response.StatusCode = 404;
     
        return View("~/Views/StandardPage/Index.cshtml", model);
    }
    

    I get the following error in PageViewContextFactory.cs:

    The provided content link does not have a value.
    Parameter name: contentLink


    Line 55:         public virtual IContent GetSection(ContentReference contentLink)
    Line 56:         {
    Line 57: var currentContent = _contentLoader.Get<IContent>(contentLink);
    Line 58:             if(currentContent.ParentLink != null && currentContent.ParentLink.CompareToIgnoreWorkID(ContentReference.StartPage))
    Line 59:             {

    I'm obviously missing something, any ideas?

    #111668 Oct 17, 2014 11:06
  •  

    Hi Mark,

    I recently had a similiar problem with this. In my MenuList-helper the code helper.ViewContext.RequestContext.GetContentLink() returned a null value. My site is an epi 7-site upgraded to 7.5. It seems that the original code 

    ControllerContext.RouteData.DataTokens[RoutingConstants.NodeKey] = startPage.NotFoundPage;

    no longer gives the desired result. I replaced this with a new (?) method that works:

    ControllerContext.RequestContext.SetContentLink(startpage.NotFoundPage);
    #113187 Nov 14, 2014 13:17
  • Henrik Fransas
    Member since: 2007
     

    You might need to set the startpage as the rootResolve, like this:

                var urlSegmentRouter = context.Locate.Advanced.GetInstance<IUrlSegmentRouter>();
                urlSegmentRouter.RootResolver = (s) => s.StartPage;
                routingParameters.UrlSegmentRouter = urlSegmentRouter;

    See more here:
    http://world.episerver.com/Forum/Developer-forum/-EPiServer-75-CMS/Thread-Container/2014/6/GetVirtualPathSegment-in-custom-segment-is-never-called/

    #113190 Nov 14, 2014 13:48
  •  
    Thanks @David,
    This worked for us also,
    <episerver>
      <applicationsettings globalerrorhandling="Off">
    </applicationsettings></episerver>
    #140321 Edited, Oct 15, 2015 21:44