Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Andy Hale 9 posts 86 karma points
    Jul 25, 2021 @ 15:22
    Andy Hale
    1

    Custom Error Pages in Umbraco 9

    Hi everyone :)

    I've been playing with, (and really enjoying), the Umbraco 9 RC and have just gotten to the point where I'm looking at adding some custom error pages.

    In previous versions I'd have added some configuration in the umbracoSettings.config file that looked something like the snippet below to serve an error page appropriate for the status code and chosen language.

    <errors>
          <error404>
            <errorPage culture="default">//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Page Not Found']</errorPage>
            <errorPage culture="en">//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Page Not Found']</errorPage>
            <errorPage culture="cy">//rootNode//errorPageFolder[@nodeName='Gwall']//errorPage[@nodeName='Tudalen Heb ei Darganfod']</errorPage>
          </error404>
          <error500>
            <errorPage culture="default">//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Internal Server Error']</errorPage>
            <errorPage culture="en">//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Internal Server Error']</errorPage>
            <errorPage culture="cy">//rootNode//errorPageFolder[@nodeName='Gwall']//errorPage[@nodeName='Gwall Gweinydd Mewnol']</errorPage>
          </error500>
        </errors>
    

    I've not seen anything similar to this in the Umbraco 9 config docs. There is a reference to a Error404Collection but I didn't see anything for different status codes, unless I've missed it of course.

    I wondered if anyone else has started looking at custom error pages in Umbraco 9 yet. Do we know if the intention is to still configure them in Umbraco, or would the approach now be to to configure custom error pages in the middleware pipeline as with other .Net 5 applications?

    I've had a little go at at a request re-execution with something like the snippet below, to try and load a general error page on a route managed by Umbraco, but I was still served the "this page is intentionally left ugly ;)" page.

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseStatusCodePagesWithReExecute("/error/general-error-page", "?code={0}");
    }
    

    I just wondered if anyone else has had a go at custom error pages yet and if so which approach is recommended?

    Thanks! :)

  • Andy Hale 9 posts 86 karma points
    Aug 08, 2021 @ 12:23
    Andy Hale
    4

    I had missed the 404 pages in the documentation! (Ooopsie), but just in case anyone else stumbles across this post and finds it's useful: -

    I've configured appsetting.json with XPath as I would previously. I'm a huge uSync fan and when restoring a website with uSync, to my knowledge, the page IDs may change; so I've tended to use XPath for custom error pages to save having to adjust IDs after a restore.

    To serve a default 404 page for each language this is working for me...

      "Umbraco": {
        "CMS": {
          "Content": {
            "Error404Collection": [
              {
                "ContentXPath": "//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Page Not Found']",
                "Culture": "default"
              }
            ]
          },
        ...
    

    To serve different 404 pages for specific languages this is working for me...

    "Umbraco": {
        "CMS": {
          "Content": {
            "Error404Collection": [
              {
                "ContentXPath": "//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Page Not Found']",
                "Culture": "default"
              },
              {
                "ContentXPath": "//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Page Not Found2']",
                "Culture": "en"
              },
              {
                "ContentXPath": "//rootNode//errorPageFolder[@nodeName='Error']//errorPage[@nodeName='Page Not Found3']",
                "Culture": "cy"
              }
            ]
          },
        ...
    

    I'm still looking for a way to serve a custom 500 error page that I can keep in the CMS. So still playing and still really enjoying V9 so far :)

  • Javz 38 posts 141 karma points
    Aug 15, 2021 @ 23:34
    Javz
    0

    Hey Andy!

    Ironically, the above you used did not work for me on the RC version of Umbraco V9, however I used the ContentLastChanceFinder for the 404 and that worked - https://our.umbraco.com/Documentation/Reference/Routing/Request-Pipeline/IContentFinder-v9

    Any luck with the 500 and other error pages? I've tried the status code implementation, but can't manage to his the custom error handles as suggested here -https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-3.1

    Thanks!

  • Andy Hale 9 posts 86 karma points
    Aug 17, 2021 @ 00:31
    Andy Hale
    1

    Hey Javz,

    I haven't found of a way to do 500s with an Umbraco 9 setting yet, but I have got them working with some middleware. The very first part of my middleware pipeline starts with this: -

    if (env.IsDevelopment())
    {
       app.UseDeveloperExceptionPage();
    }
    else
    {
       app.UseExceptionHandler("/error/internal-server-error/");
       app.UseHttp200StatusCodesForExceptions();
     }
    

    The string in UseExceptionHandler middleware is the path to a page I have published in Umbraco and the second line is another little bit of middleware to change the status code from a 500 to a 200, just in case any fuzzers are poking around for errors :)

    I've not had chance to look at different routes for different languages just yet, nor have I had chance to look at any other status codes with UseStatusCodePages(), but I'm hoping to find time to have another play this week. If I get any further, I'll post here.

    Hope this is helpful.

  • Javz 38 posts 141 karma points
    Aug 20, 2021 @ 17:49
    Javz
    0

    Thanks Andy!

    Did you try to throw a new Exception in one of your view files to see if the specified page set in the middleware displays? As I've done the same and it didn't seem to work for me.

    I've also tried the status codes with both redirect and reexecute, and neither seemed to have worked for me sadly :(

  • Andy Hale 9 posts 86 karma points
    Aug 21, 2021 @ 12:16
    Andy Hale
    0

    Hi Javz,

    Yep, I just did some route hijacking to throw a Not Implemented Exception to see if it worked. Here's what I have in my controller: -

    public class ContentPageWithGridLayoutController : RenderController
    {
        public ContentPageWithGridLayoutController(
            ILogger<ContentPageWithGridLayoutController> logger, 
            ICompositeViewEngine compositeViewEngine, 
            IUmbracoContextAccessor umbracoContextAccessor) 
        : base(logger, compositeViewEngine, umbracoContextAccessor) { }
    
    
        //
        // RenderController Implementation
    
        public override IActionResult Index()
        {
            throw new NotImplementedException();
        }
    }
    

    I wonder if its not detecting that you're in the Production Environment. What happens if you change the beginning of your Middleware in Startup.cs from this: -

      public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
      {
       if (env.IsDevelopment())
       {
          app.UseDeveloperExceptionPage();
       }
       else
       {
          app.UseExceptionHandler("/error/internal-server-error/");
       }
       ...
     }
    

    To this: -

     public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
      {
         app.UseExceptionHandler("/error/internal-server-error/");
         ...
      }
    

    This will mean we'll lose the developer exception pages, because we're always returning an exception page now, but if it works, that gives us a clue that the problem is with the environment rather than the middleware. :)

  • Javz 38 posts 141 karma points
    Sep 29, 2021 @ 22:28
    Javz
    0

    Apologies for the very late reply! I've tried that but I get the following error: enter image description here

    I researched the error, and it could be my local setup perhaps? https://stackoverflow.com/questions/41992280/http-error-500-localhost-is-currently-unable-to-handle-this-request

    Thanks again Andy!

  • Rick Mason 10 posts 93 karma points
    Sep 29, 2021 @ 20:39
    Rick Mason
    0

    I've done some experimenting and found that, if you're proxying through IIS like on Umbraco Cloud, the old approach of controlling error responses there still works, so your web.config file becomes:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <location path="." inheritInChildApplications="false">
        <system.webServer>
          <handlers>
            <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
          </handlers>
          <aspNetCore processPath="dotnet" arguments=".\UmbracoProject.dll" stdoutLogEnabled="false" stdoutLogFile="\\?\%home%\LogFiles\stdout" hostingModel="inprocess" />
    
          <httpErrors errorMode="Custom" existingResponse="Replace">
            <remove statusCode="400" subStatusCode="-1" />
            <error statusCode="400" subStatusCode="-1" path="/errors/400" responseMode="ExecuteURL" />
            <remove statusCode="404" subStatusCode="-1" />
            <error statusCode="404" subStatusCode="-1" path="/errors/404" responseMode="ExecuteURL" />
            <remove statusCode="500" subStatusCode="-1" />
            <error statusCode="500" subStatusCode="-1" path="/errors/500" responseMode="ExecuteURL" />
          </httpErrors>
        </system.webServer>
      </location>
    </configuration>
    

    A few things to note:

    • it supports status codes other than 404/500, as with 400 shown here. Add as many as you need.
    • the route can be an Umbraco page, a static file, or probably an MVC action (I didn't try the last one)
    • if you have app.UseExceptionHandler("/errors/500") that path is used in preference to the one in web.config
    • if you have Error404Collection configured in appsettings, web.config takes precedence over that (I haven't tried the LastChanceContentFinder approach)
    • dotnet run on your local machine isn't usually behind an IIS proxy, so if you try this there it probably won't work

    On a v8 project you could make this config change when deploying to Umbraco Cloud by using web.live.xdt.config file. That doesn't appear to work any more, so I'm not yet sure how to deploy this to Umbraco Cloud. I've tested it by editing the file manually in Kudu.

  • Javz 38 posts 141 karma points
    Sep 29, 2021 @ 22:32
    Javz
    0

    Hi Rick,

    I was under the impression web.config files were no longer in use Umbraco 9?

    Can you please let me know your setup and if the file is as it has been on previous versions.

    And I've noticed you also have app.UseExceptionHandler("/errors/500") working too, can you let me know how you've done it?

    Thanks!

  • Andy Hale 9 posts 86 karma points
    Oct 10, 2021 @ 15:49
    Andy Hale
    0

    Do you have the project checked into a public repository Javz?
    Happy to take a look to see if I can spot it.

  • Arjan H. 226 posts 463 karma points c-trib
    Jun 11, 2022 @ 10:34
    Arjan H.
    0

    I'm not sure why, but using the web.config to configure custom HTTP errors somehow broke the backoffice in my Umbraco 10 project. /umbraco returned a 403 Access Denied. It worked fine in Umbraco 9 though.

    I ended up using the following code-based solution:

    Create the following views:

    ~/Views/Error/404.cshtml
    ~/Views/Error/500.cshtml
    

    Implement the following controller:

    [Route("error")]
    public class ErrorController : Controller
    {
        public ErrorController()
        {
        }
    
        [Route("500")]
        public IActionResult AppError()
        {
            return View("~/Views/Error/500.cshtml");
        }
    
        [Route("404")]
        public IActionResult PageNotFound()
        {
            return View("~/Views/Error/404.cshtml");
        }
    }
    

    And add this to Startup.cs:

    // 500 errors
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/error/500");
    }
    
    // 404 errors
    app.Use(async (context, next) =>
    {
        await next();
    
        if (context.Response.StatusCode == 404 && context.Response.HasStarted == false)
        {
            //Re-execute the request so the user gets the error page
            context.Request.Path = "/error/404";
            await next();
        }
    });
    

    Of course you could add some additional telemetry/logging to the ErrorController methods.

    Umbraco's 404 handlers pick up the page not found first, but for anything that's missed by the handlers (like non-directory URL's, e.g. /file-does-not-exist.txt) the code above kicks in.

    Reference: https://joonasw.net/view/custom-error-pages

  • Rick Mason 10 posts 93 karma points
    Sep 29, 2021 @ 22:49
    Rick Mason
    0

    In v8 and below web.config was used by three layers, the web server (IIS), ASP.NET, and Umbraco. With v9, Umbraco and ASP.NET don't use it any more, and while you still have a web server it doesn't have to be IIS, so that's why you've heard that it's gone away. However if you are using IIS then web.config still has a role to play.

    app.UseExceptionHandler("/errors/500") was done exactly as Andy describes. It just worked for me, but I wanted to handle the other codes too so I kept looking. I was actually using it with a static HTML file in the wwwroot folder so my line was app.UseExceptionHandler("/500.html"), but all the examples I saw online show it should work with an MVC route too.

  • Rick Mason 10 posts 93 karma points
    Oct 05, 2021 @ 18:09
    Rick Mason
    0

    There is a way to deploy config like this on Cloud - name your transform file Web.Release.config and put it in the src\UmbracoProject folder (or wherever you changed the reference in the .umbraco file to point to).

    The #h5yr goes to @c9mbundy on this Github issue.

  • Tom van Enckevort 107 posts 429 karma points
    Apr 04, 2022 @ 10:55
    Tom van Enckevort
    0

    I'm trying to get the custom error page for HTTP status code 500 to work using UseExceptionHandler, but while it works and displays the correct page, the HTTP status code for the response is 200 whereas I would like it to be 500 (so we can pick up any errors in our analytics logs as well).

    The Configure method in our Startup.cs looks like this:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/500.html");
        }
    
        app.UseUmbraco()
            .WithMiddleware(u =>
            {
                u.UseBackOffice();
                u.UseWebsite();
                u.UseAzureBlobMediaFileSystem();
            })
            .WithEndpoints(u =>
            {
                u.UseInstallerEndpoints();
                u.UseBackOfficeEndpoints();
                u.UseWebsiteEndpoints();
            });
    }
    

    If I intercept the response for a failing request (i.e. be throwing an exception on the homepage template) between the UseExceptionHandler call and the app.UseUmbraco call I can see that the response status code at that point is set to 500:

    app.Use(async (context, next) =>
    {
        Debug.WriteLine(context.Response.StatusCode);
    
        await next();
    });
    

    Could it be that the Umbraco pipeline somewhere changes the response status code back from 500 to 200? And if so, how could we prevent that from happening?

  • Andy Hale 9 posts 86 karma points
    Apr 04, 2022 @ 12:18
    Andy Hale
    0

    Hi Tom, I can’t say that I’ve tried that before. The advice I‘ve always been given by security folks is to suppress the 500s and return a 200 instead. The reason being that automated tools looking for weaknesses on a website will throw all sorts or random requests at a page and if a 500 is returned at any point, this may highlight a weakness and then they have a place to focus their attention on to keep digging for weakness’s or sensitive information disclosure.

    I’m not near a computer to confirm in the source code at the moment, but I wouldn’t be surprised if the default behaviour of ASP .Net 5 is to rewrite the request to a 200. I’d have to look later though. Personally I quite like the approach of suppressing 500s for 500 pages.

    What are you using for logging? Could you keep returning the 200 to suppress the issue from the fuzzers, but still log the error? If it’s application insights there are some APIs for you to write directly to the logs. The Umbraco Logger has overloads for logging exceptions at the ‘Error’ level as well, so that may be an option to keep them all in one place. You may find they are already in there though.

    Hope this helps, though I understand if it’s not the approach that you were looking to take.

    Andy

  • Andy Hale 9 posts 86 karma points
    Apr 04, 2022 @ 12:25
  • Tom van Enckevort 107 posts 429 karma points
    Apr 04, 2022 @ 12:53
    Tom van Enckevort
    0

    Yes, you might be right about the re-execution bit. Will have another dig around to see what I can find.

    Thanks for your replies and suggestions.

  • Tom van Enckevort 107 posts 429 karma points
    Apr 05, 2022 @ 15:01
    Tom van Enckevort
    0

    So I managed to get round the issue by using the following code:

    var errorFilePath = Path.Combine(env.WebRootPath, "500.html");
    
    app.UseExceptionHandler("/500.html");
    app.Use(async (context, next) =>
    {
        context.Response.OnStarting(async () =>
        {
            if (context.Request.Path == "/500.html")
            {
                context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            }
            else if (context.Response.StatusCode >= StatusCodes.Status400BadRequest && context.Response.StatusCode != StatusCodes.Status404NotFound)
            {
                var statusCode = context.Response.StatusCode;
    
                var page = await File.ReadAllTextAsync(errorFilePath);
    
                context.Response.Clear();
                context.Response.StatusCode = statusCode;
    
                await context.Response.WriteAsync(page);
            }
        });
    
        await next();
    });
    

    That takes care of setting the error status code before the response is sent back to the client for both unhandled exceptions, where the UseExceptionHandler middleware will render the 500.html file and we just need to change the status code, and also for other error types (like bad requests returned from controllers) by clearing the default response and including the content of the 500.html file (you can use different files for different errors when required) and setting the right status code.

Please Sign in or register to post replies

Write your reply to:

Draft