77

In a old site, I was changing the way that CustomErrors works by adding redirectMode="ResponseRewrite" (new in 3.5 SP1):

<customErrors mode="RemoteOnly" defaultRedirect="Error.aspx" redirectMode="ResponseRewrite">
    <error statusCode="404" redirect="404.aspx" />
</customErrors> 

The thing is: it shows me the generic error page (the one that you get when you don't set customErrors. If I remove theredirectMode="ResponseRewrite" part, it works fine.

I'm sure 3.5 SP1 is installed in the server, because I use the same setting on other sites hosted in the same server.

Any ideas?

alex
  • 6,818
  • 9
  • 52
  • 103
Eduardo Molteni
  • 38,786
  • 23
  • 141
  • 206

10 Answers10

105

It is important to note for anyone trying to do this in an MVC application that ResponseRewrite uses Server.Transfer behind the scenes. Therefore, the defaultRedirect must correspond to a legitimate file on the file system. Apparently, Server.Transfer is not compatible with MVC routes, therefore, if your error page is served by a controller action, Server.Transfer is going to look for /Error/Whatever, not find it on the file system, and return a generic 404 error page!

Mike Atlas
  • 8,193
  • 4
  • 46
  • 62
Michael Hallock
  • 1,413
  • 1
  • 11
  • 17
  • I notice a problem also with Web Form and Routing see my questions here http://stackoverflow.com/questions/7269103/problem-with-defaultredirect-in-web-config-customerrors – GibboK Sep 01 '11 at 12:06
  • There is a CodePlex issue for allowing ResponseRewrite to work with MVC routes, please vote: http://aspnet.codeplex.com/workitem/9034 – Dmitry Sep 24 '13 at 13:31
  • 1
    @PussInBoots, here's a archived link https://web.archive.org/web/20131201222548/http://aspnet.codeplex.com/workitem/9034 – KyleMit Dec 01 '14 at 22:28
56

The only way that worked perfectly for me is to turn off custom errors and replace iis's error pages via web.config. It sends the correct status code with the response and has the benefit of not going through the mvc.

here's the code

  1. Turn off custom errors

    <customErrors mode="Off" />
    
  2. Replace error pages

    <httpErrors errorMode="Custom" existingResponse="Replace">
      <remove statusCode="404" subStatusCode="-1" />
      <remove statusCode="500" subStatusCode="-1" />
      <error statusCode="404" path="Error404.html" responseMode="File" />
      <error statusCode="500" path="Error.html" responseMode="File" />
    </httpErrors>
    

Note. Use responsemode="file" if the url is a direct link to a file

info : http://tipila.com/tips/use-custom-error-pages-aspnet-mvc

JT.
  • 469
  • 6
  • 9
Amila
  • 2,779
  • 1
  • 27
  • 31
  • 1
    see http://meta.stackexchange.com/a/22189/147333 for code formatting inside a list :) – CharlesB May 15 '12 at 06:17
  • 3
    I have drove myself insane trying to find the "right" way to handle errors for my application and this was the solution. I still need to handle some errors in the global but that is OK because I want those logged. When that happens I am doing a Server.Transfer to an aspx error page. The greatest part of this solution is the user never knows what my handler is named and is never taken to a URL they didn't request. – Chris Porter Oct 09 '12 at 21:52
  • 3
    This solution is far from perfect, it will replace AJAX and API error responses http://stackoverflow.com/questions/24465261/customerrors-vs-httperrors-a-significant-design-flaw – Guillaume Dec 05 '14 at 10:08
  • I had set existingResponse="Auto" which caused all sorts of problems as it worked with 404 but not 500 errors. Thanks! If you want to remove all previous httpErrors, I found you can use instead of individual I also found that I could not pass 500 errors to a controller and set responseMode=ExecuteURL as I had done for 404 errors which worked. – Peheje May 18 '18 at 08:28
20

What's happening is IIS is seing the error status code and presenting it's own error page instead of yours. To solve you need to set this in the code behind page of your error page to prevent IIS from doing this:

Response.TrySkipIisCustomErrors = true;

This will only work in IIS7 or above, for earlier versions of IIS you'll need to play with the error page settings.

Michael
  • 11,571
  • 4
  • 63
  • 61
  • 2
    Thanks - this is the solution if you need to use a .aspx page as the defaultRedirect. – frankadelic Aug 18 '10 at 22:57
  • 1
    Thanks for telling me about TrySkipIisCustomErrors. I have to switch back to ResponseRedirect since we have to stick with IIS 6 though... – Vinz Oct 12 '10 at 14:29
  • I've added a bit more info about TrySkipIisCustomErrors in this answer http://stackoverflow.com/a/21271085/222748 – Michael May 04 '15 at 11:57
16

Due to the reliance on Server.Transfer it seems that the internal implementation of ResponseRewrite isn't compatible with MVC.

This seems like a glaring functionality hole to me, so I decided to re-implement this feature using a HTTP module, so that it just works. The solution below allows you to handle errors by redirecting to any valid MVC route (including physical files) 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)

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() {

        }


    }

}

Usage

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>
Red Taz
  • 4,159
  • 4
  • 38
  • 60
  • Thank you for the solution. I have placed the code to global.asax but status still was 200. To solve the problem with integrated pipleine we need to set status earlier. So, I was able to put it in 404 page action: [AllowAnonymous] public ActionResult NotFound() { Response.StatusCode = 404; return View("NotFound"); } – Pavel Korsukov Nov 04 '15 at 13:11
  • I've been using this method happily but recently found it can interfere with IIS application initialization. Haven't had a chance to dig into it, but when the error handler is loaded via web.config it appears to take precedence over the initialization process. As a result, when attempting to use the `remapManagedRequestsTo` attribute in combination with `skipManagedModules="true"` there can be a significant delay before the remap page loads. I was able to work around the issue by [loading the module via app initialization](https://stackoverflow.com/a/5832092/264628). – BrianS Nov 28 '19 at 02:32
  • You should consider publishing this as a Nuget Package, so you get more credit for it, rather than all of us copy-pasting your code into our own namespaces. – Chris Moschini Dec 29 '21 at 14:54
10

I know this question is a bit old, but I thought I should point out that it doesn't need to be a static file to get this working.

I ran into a similar thing, and it's just a matter of finding that error in your Error.aspx, in our case it was because the masterpage in use relied on a piece of session data and when ResponseRewrite was set the session is not available to our Error.aspx page.

I haven't worked out yet whether this unavailability of session is due to our specific app config or a "by design" part of ASP.net.

Chris
  • 224
  • 3
  • 6
1

I built an error page in aspx that transfers the query to an ASP.NET MVC controller. You can rewrite the query to this aspx page and it will transfer the query to your custom controller.

protected void Page_Load(object sender, EventArgs e)
{
  //Get status code
  var queryStatusCode = Request.QueryString.Get("code");
  int statusCode;
  if (!int.TryParse(queryStatusCode, out statusCode))
  {
    var lastError = Server.GetLastError();
    HttpException ex = lastError as HttpException;
    statusCode = ex == null ? 500 : ex.GetHttpCode();
  }
  Response.StatusCode = statusCode;

  // Execute a route
  RouteData routeData = new RouteData();
  string controllerName = Request.QueryString.Get("controller") ?? "Errors";
  routeData.Values.Add("controller", controllerName);
  routeData.Values.Add("action", Request.QueryString.Get("action") ?? "Index");

  var requestContext = new RequestContext(new HttpContextWrapper(Context), routeData);
  IController controller = ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, controllerName);
  controller.Execute(requestContext);
}

Find more details here : https://stackoverflow.com/a/27354140/143503

Community
  • 1
  • 1
Guillaume
  • 12,824
  • 3
  • 40
  • 48
  • I combined the solution offered here with http://stackoverflow.com/a/5536676/2310818 to take care of all sorts of errors whether caused due to unknown route, unknown controller, unknown action, HttpNotFound() result returned by the controller action, and HttpException thrown by a controller. I achieved all of this while achieving correct status codes (404, 500) depending on type of error code. – Parth Shah Apr 27 '15 at 09:57
  • 1
    @ParthShah, it worked with this solution alone but maybe with some restriction on how you return errors (throw an exception instead of returning a result I guess but I don't really remember). Error handling with ASP MVC/Web API and IIS is a pain, glad you managed to make it work ;) – Guillaume Apr 27 '15 at 12:11
1

According to @Amila's post and confirmation and completion of that post, I have same problem, I dig a lot google but had no chance to find the correct answer. The problem is when you are working with ASP.Net Web Application, whether it's an MVC or not you can't achieve custom error using the old way with Webform project.
Here the Option if you are using ASP.Net Web Application (whether it's an MVC or not):

In my scenarios I just want to define a custom error for a specific 404 error, The other error defined same as 404 error:


Senario1: Your custom page is a simple HTML file and placed in the root:

<configuration>
   <system.web>
      <customErrors mode="Off" />
   </system.web>
   <system.webServer>
       <httpErrors errorMode="Custom" existingResponse="Replace">
           <remove statusCode="404" subStatusCode="-1" />
           <error statusCode="404" path="ErrorPage.html" responseMode="File" />
       </httpErrors>
   </system.webServer>
</configuration>



Senario2: Your custom page is an aspx page and placed in the root:
<configuration>
   <system.web>
      <customErrors mode="Off" />
   </system.web>
   <system.webServer>
       <httpErrors errorMode="Custom" existingResponse="Replace">
           <remove statusCode="404" subStatusCode="-1" />
           <error statusCode="404" path="ErrorPage" responseMode="Redirect" />
       </httpErrors>
   </system.webServer>
</configuration>

Note: I remove the aspx extension due to RouteConfig.cs in ASP.net application, you can use ErrorPage.aspx if you like, it's optional.


Senario3: Your custom page is an aspx page and placed in the [ex: Page folder in The root (~/Page/ErrorPage.aspx)]:
The tip here that I noticed is YOU SHOULD NOT USE~/ to the root addressing; So I just addresing without ~/ mark:
<configuration>
   <system.web>
      <customErrors mode="Off" />
   </system.web>
   <system.webServer>
       <httpErrors errorMode="Custom" existingResponse="Replace">
           <remove statusCode="404" subStatusCode="-1" />
           <error statusCode="404" path="Page/ErrorPage" responseMode="Redirect" />
       </httpErrors>
   </system.webServer>
</configuration>
Reza Paidar
  • 863
  • 4
  • 21
  • 51
1

I found that the problem was in Error.aspx. Still can't find what was the actual error in error.aspx that causes the problem.

Changing the page to a static html file solved the problem.

Eduardo Molteni
  • 38,786
  • 23
  • 141
  • 206
0

In my particular case, my error page had a master page that had a user control that tried to use Session. If Session isn't available, you get an HttpException: "Session state can only be used when enableSessionState is set to true, either in a configuration file or in the Page directive." Easiest fix is to switch to static html, second easiest fix is to use a simpler error page, hardest fix is to make incredibly sure that your error page makes no assumptions anywhere (like that Session won't throw an exception, for example) and can't possibly error out.

David Eison
  • 1,367
  • 12
  • 19
0

I have found out that if you use redirectMode="ResponseRewrite" then you need to add something in the rewrite area of the web.config file. Problem is when your site is broken! You can't URL rewrite as your site can't call the "virtual.aspx" that handles your rewrite!

Colin Wiseman
  • 848
  • 6
  • 10