November Happy Hour will be moved to Thursday December 5th.

Custom 404 page in EPi 7 MVC

Vote:
 

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

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

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

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

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

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

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

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

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

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

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

David, nice! :-)

#70614
Apr 24, 2013 12:51
Vote:
 

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

Dave, glad you found it useful! :-)

#73585
Jul 30, 2013 14:39
Vote:
 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
Vote:
 
Thanks @David,
This worked for us also,
<episerver>
  <applicationsettings globalerrorhandling="Off">
</applicationsettings></episerver>
#140321
Edited, Oct 15, 2015 21:44
This thread is locked and should be used for reference only. Please use the Episerver CMS 7 and earlier versions forum to open new discussions.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.