In my MVC web project. I am trying to show custom error pages to my visitors without using "custromerrors" element in web.config.
I can catch exceptions like below
protected void Application_Error(object sender, EventArgs e)
{
Exception exception = Server.GetLastError();
bool success = RaiseErrorSignal(exception);
Response.Clear();
HttpException httpException = exception as HttpException;
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
if (httpException == null)
{
routeData.Values.Add("action", "Index");
}
else //It's an Http Exception, Let's handle it.
{
switch (httpException.GetHttpCode())
{
case 404:
// Page not found.
routeData.Values.Add("action", "Error404");
break;
case 500:
// Server error.
routeData.Values.Add("action", "Error500");
break;
// Here you can handle Views to other error codes.
// I choose a General error template
default:
routeData.Values.Add("action", "Index");
break;
}
}
// Pass exception details to the target error View.
routeData.Values.Add("error", exception);
// Clear the error on server.
Server.ClearError();
// Call target Controller and pass the routeData.
IController errorController = new ProjectName.WebSite.Controllers.ErrorController();
errorController.Execute(new RequestContext(
new HttpContextWrapper(Context), routeData));
}
private static bool RaiseErrorSignal(Exception e)
{
var context = HttpContext.Current;
if (context == null)
return false;
var signal = ErrorSignal.FromContext(context);
if (signal == null)
return false;
signal.Raise(e, context);
return true;
}
But Elmah cant log errors also i am raising error signal.
I found the problem, I missed a web.config section. I added "ErrorLog" module to <system.webserver><modules>.
I also need to add it to <system.web><httpModules>.
After adding, Elmah start to log errors.
Also I don't need to call ErrorSignal.Raise() method, Elmah can detect errors without signalling.
Related
I created a custom exception handling method as shown below and I can catch the database constraint exception with it and return the error to error method of AJAX call. However, when trying to create an exception using throw new Exception()" or throw new ArgumentNullException("instance") I encounter an error as displayed on the image below. Is there any mistake in the custom method? Or how can I test properly test it if it works for AJAX request and Normal request? Any help would be appreciated...
public class CustomErrorHandler : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
//If the request is AJAX return JSON, else return View
if (filterContext.HttpContext.Request.IsAjaxRequest() && filterContext.Exception != null)
{
// Log exception first
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
filterContext.Result = new JsonResult
{
Data = new
{
success = false,
message = "Error occured",
type = filterContext.Exception.GetType().Name,
exception = filterContext.Exception.ToString(),
number = ((System.Data.SqlClient.SqlException)filterContext.Exception.InnerException.InnerException).Number
},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
// Let the system know that the exception has been handled
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
}
else
{
// Normal Exception. So, let it handle by its default ways
base.OnException(filterContext);
}
}
}
I'm trying to implement custom error handling in my asp.net mvc 5 application, using the same technique I have always used :
protected void Application_Error(object sender, EventArgs e)
{
try
{
HandleApplicationErrors();
Response.TrySkipIisCustomErrors = true;
}
catch(Exception ex)
{
Response.Write(ex.ToString());
Response.StatusCode = 500;
}
}
private void HandleApplicationErrors(int? statusCode = null)
{
try
{
Exception ex = Server.GetLastError();
Response.Clear();
HttpException httpEx = ex as HttpException;
if (statusCode == null && httpEx != null) statusCode = httpEx.GetHttpCode();
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
switch (statusCode)
{
case 404:
routeData.Values.Add("action", "NotFound");
break;
case 403:
routeData.Values.Add("action", "Forbidden");
break;
default:
routeData.Values.Add("action", "ServerError");
break;
}
routeData.Values.Add("exception", ex);
Server.ClearError();
this.Server.ClearError();
this.Response.TrySkipIisCustomErrors = true;
IController controller = new ErrorController();
controller.Execute(new RequestContext(new HttpContextWrapper(this.Context), routeData));
}
catch(Exception ex)
{
Response.Write(ex.ToString());
Response.StatusCode = 500;
}
}
This has worked just fine in the past, however in this application, I'm using Autofac, and it's throwing "Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed." on the controller.Execute line. I've tried excluding the ErrorController from autofac registration (it doesn't have and constructor parameters anyway) and constructing it directly.
The controller code is pretty simple :
private CustomErrorModel GetModel(Exception ex)
{
var model = new CustomErrorModel();
model.RequestedUrl = Request.Url.OriginalString;
model.ReferrerUrl = (Request.UrlReferrer == null || model.RequestedUrl == Request.UrlReferrer.OriginalString) ? null : Request.UrlReferrer.OriginalString;
model.Exception = ex;
return model;
}
public ActionResult ServerError(Exception ex)
{
var model = GetModel(ex);
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
Response.Status = "500 Internal Server Error";
return View(model);
}
Anyone have any ideas on making this work?
I am doing a very similar thing in my application, including using Autofac for dependency injection, and have not seen the same issues as you.
Looking closely at your HandleApplicationErrors method, I notice that your route data does not match the parameter that your ServerError action is expecting.
In HandleApplicationErrors, you are passing in a parameter named exception
In ServerError, you are expecting a parameter named ex
Updating the setting of the route data to match like so:
routeData.Values.Add("ex", ex);
will resolve the issue.
Ok, answering my own question here. I had a look at stack trace, and it seemed the exception was caused by mvc trying to use the container to find the model binder.. so I made my action methods parameterless and that solved it!
Posting code for reference here :
protected void Application_Error(object sender, EventArgs e)
{
try
{
HandleApplicationErrors();
Response.TrySkipIisCustomErrors = true;
}
catch(Exception ex)
{
Response.Write(ex.ToString());
Response.StatusCode = 500;
}
}
private void HandleApplicationErrors(int? statusCode = null)
{
try
{
Exception ex = Server.GetLastError();
Response.Clear();
HttpException httpEx = ex as HttpException;
if (statusCode == null && httpEx != null) statusCode = httpEx.GetHttpCode();
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
switch (statusCode)
{
case 404:
routeData.Values.Add("action", "NotFound");
break;
case 403:
routeData.Values.Add("action", "Forbidden");
break;
default:
routeData.Values.Add("action", "ServerError");
break;
}
//routeData.Values.Add("exception", ex);
Server.ClearError();
this.Server.ClearError();
this.Response.TrySkipIisCustomErrors = true;
IController controller = new ErrorController();
controller.Execute(new RequestContext(new HttpContextWrapper(this.Context), routeData));
}
catch(Exception ex)
{
Response.Write(ex.ToString());
Response.StatusCode = 500;
}
}
and my controller method :
public ActionResult ServerError()
{
HttpException ex = Server.GetLastError() as HttpException;
var model = GetModel(ex);
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
Response.Status = "500 Internal Server Error";
return View(model);
}
"The request lifetime itself is disposed in the EndRequest event. You
can see this in the RequestLifetimeHttpModule which gets added
automatically to your pipeline when you reference the MVC integration.
Most likely there is a race condition where the Autofac event handler
is firing before your own EndRequest event handler. This is most
likely cropping up now simply because more recent Autofac integration
tries to make the registration of the module, etc. more seamless to
the developer so we register the module on pre-application-start. We
subscribe to the event first, so we get called first.
Unfortunately, there's really not much to be done about it from the
Autofac side - EndRequest is the last event in the pipeline and we
have to dispose of the lifetime scope, so that's where it happens.
If you are handling something at EndRequest that needs to be resolved,
it may be too late. Even in previous integrations, EndRequest would
have been a risk. For example, if you resolve an object that
implements IDisposable, the release of the lifetime scope on
EndRequest would dispose of the object and you'd be working with an
object in a bad state.
I recommend trying to move the execution of your action to some time
before EndRequest. Alternatively, if you are 100% sure that the
objects in your chain aren't IDisposable, you could resolve the object
in an earlier event, store it in HttpContext.Items, and retrieve it
from there to use in your EndRequest handler."
font: https://github.com/autofac/Autofac/issues/570
I implemented a custom error handler for my MVC5 project and everything would be fine if it wasn't of the customErrors attribute. I'll explain: When I got an error in the application, I catch it inside void Application_Error from Global.asax like this:
protected void Application_Error(object sender, EventArgs e)
{
var httpContext = ((HttpApplication)sender).Context;
ExecuteErrorController(httpContext, Server.GetLastError());
}
public static void ExecuteErrorController(HttpContext httpContext, Exception exception)
{
if (!exception.Message.Contains("NotFound") && !exception.Message.Contains("ServerError"))
{
var routeData = new RouteData();
routeData.Values["area"] = "Administration";
routeData.Values["controller"] = "Error";
routeData.Values["action"] = "Insert";
routeData.Values["exception"] = exception;
using (Controller controller = new ErrorController())
{
((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}
}
}
Then, inside my ErrorController I do:
public ActionResult Insert(Exception exception)
{
ErrorSignal.FromCurrentContext().Raise(exception);
Server.ClearError();
Response.Clear();
switch (Tools.GetHttpCode(exception)) // (int)HttpStatusCode.NotFound;
{
case 400:
return RedirectToAction("BadRequest");
case 401:
return RedirectToAction("Unauthorized");
case 403:
return RedirectToAction("Forbidden");
case 404:
return RedirectToAction("NotFound");
case 500:
return RedirectToAction("ServerError");
default:
return RedirectToAction("DefaultError");
}
}
public ActionResult Unauthorized()
{
return View();
}
...
So the first time, everything works perfectly
But !! The code repeat itself because the NotFound or ServerError page aren't in the Shared folder. Those page are supposed to be set in customErrors attribute BUT the thing is I don't need it at all. I finally got this error: ERR_TOO_MANY_REDIRECTS because of that.
I read all day to find any answer about that, and it seams that everyone who published their code do the same kind of pattern as mine, and no matter what I tried, nothing works.
Notice my desperate if condition: if (!exception.Message.Contains("NotFound") && !exception.Message.Contains("ServerError"))
I also comment those two lines in the global.asax because everywhere I read, it says we need to remove them in order to get this done.
//GlobalConfiguration.Configure(WebApiConfig.Register);
//FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
Also, because of the desparate if, I got this answer:
Runtime Error
Description: An application error occurred on the server. The current custom error settings for this application prevent the details of the application error from being viewed.
Details: To enable the details of this specific error message to be viewable on the local server machine, please create a <customErrors> tag within a "web.config" configuration file located in the root directory of the current web application. This <customErrors> tag should then have its "mode" attribute set to "RemoteOnly". To enable the details to be viewable on remote machines, please set "mode" to "Off".
<!-- Web.Config Configuration File -->
<configuration>
<system.web>
<customErrors mode="RemoteOnly"/>
</system.web>
</configuration>
I also tried Response.TrySkipIisCustomErrors = true; and it doesn't work!
So, how can I get ride of customErrors completely and manage my own error handler in my project?
Alright, thanks to the comment of RoteS. I finally found what I need to get this done !
The way I did it by Executing the ErrorController wasn't good.
using (Controller controller = new ErrorController())
{
((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}
I found that by using ServerTransfert instead, we can get ride of customErrors attribute. Here is the final solution (tested):
protected void Application_Error(object sender, EventArgs e)
{
// Response.TrySkipIisCustomErrors = true; I don't know if I will need it someday.
var httpContext = ((HttpApplication)sender).Context;
var exception = Server.GetLastError();
ErrorSignal.FromCurrentContext().Raise(exception);
Server.ClearError();
Response.Clear();
string relativePath = "~/Administration/Error/{0}";
switch (Tools.GetHttpCode(exception))
{
case (int)HttpStatusCode.BadRequest:
Server.TransferRequest(string.Format(relativePath, "BadRequest"));
break;
case (int)HttpStatusCode.Unauthorized:
Server.TransferRequest(string.Format(relativePath, "Unauthorized"));
break;
case (int)HttpStatusCode.Forbidden:
Server.TransferRequest(string.Format(relativePath, "Forbidden"));
break;
case (int)HttpStatusCode.NotFound:
Server.TransferRequest(string.Format(relativePath, "NotFound"));
break;
case (int)HttpStatusCode.InternalServerError:
Server.TransferRequest(string.Format(relativePath, "ServerError"));
break;
default:
Server.TransferRequest(string.Format(relativePath, "DefaultError"));
break;
}
}
Thanks to RoteS for the comment that pointed me in the right direction.
David
I've developed an MVC 4 site which runs correctly on my local Win 7 workstation. My workstation has MVC 4 installed as part of the Visual Studio 2010 bolt-on.
I've deployed the app to my DEV server, which is Windows Server 2008 R2. Note that MVC 4 is NOT installed onto the DEV server, instead the app uses the MVC bin deployables. In some cases my error controller gets called but I'm not sure why. Here's the exception:
System.InvalidOperationException: The view 'Error' or its master was
not found or no view engine supports the searched locations. The
following locations were searched:
~/Views/Customer/Error.cshtml
~/Views/Customer/Error.vbhtml
~/Views/Shared/Error.cshtml
~/Views/Shared/Error.vbhtml
The stack trace does not show any line numbers as to where the exception originates. By the exception it appears MVC is expecting to find view associated with the Customer controller first then checks shared. However, there is no view in either path and there SHOULDN'T be.
The site uses a global error handler via Application_Error in global.asax.cs:
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError().GetBaseException();
ILogger httplog = new HttpLogIt(new HttpContextWrapper(Context));
if (new HttpRequestWrapper(Request).IsAjaxRequest())
{
httplog.Error(1, Enums.ErrorCode.INTERNAL_SERVER_ERROR, "An application error occurred during an AJAX request. See exception for details.", ex, false);
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
Response.ContentType = "application/json";
Response.Write(new JavaScriptSerializer().Serialize(new
{
errorMessage = "We apologize, the website has experienced an error. Please try again."
}));
return;
}
else
{
httplog.Error(1, Enums.ErrorCode.INTERNAL_SERVER_ERROR, "An application error occurred. See exception for details.", ex, false);
}
Response.Clear();
// Clear the error on server.
Server.ClearError();
// Avoid IIS7 getting in the middle
Response.TrySkipIisCustomErrors = true;
// try to send error info to Error controller
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
// maintain current url, even if invalid, when displaying error page
routeData.Values.Add("url", Context.Request.Url.OriginalString);
if (ex is HttpException)
{
HttpException httpException = ex as HttpException;
switch (httpException.GetHttpCode())
{
case 404:
// Page not found.
routeData.Values.Add("action", "Http404");
break;
default:
routeData.Values.Add("action", "Unavailable");
break;
}
}
else
{
routeData.Values.Add("action", "Unavailable");
}
// Pass exception details to the target error View.
var model = new HandleErrorInfo(ex, routeData.Values["controller"].ToString(), routeData.Values["action"].ToString());
routeData.Values.Add("errorinfo", model);
// Call target Controller and pass the routeData.
IController errorController = new ErrorController();
errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
}
The re-route from the global handler to the Error controller lands in the the Error controller where it explicitly sets the views to use:
[SessionState(SessionStateBehavior.Disabled)]
public class ErrorController : Controller
{
private ILogger _httplog;
public ErrorController()
{
_httplog = new HttpLogIt(this.HttpContext, this.RouteData);
}
public ActionResult Http404()
{
if (this.ControllerContext.RouteData.Values["errorinfo"] != null)
{
HandleErrorInfo errorinfo = null;
errorinfo = (HandleErrorInfo)this.ControllerContext.RouteData.Values["errorinfo"];
_httplog.Warn(1, Enums.WarningCode.PAGE_NOT_FOUND, "A global application exception was handled and the end user was redirected to the Error Controller with Http404 Action.");
}
Response.StatusCode = (int)HttpStatusCode.NotFound;
// explicitly set the View below
return View("Error404");
}
public ActionResult Http500()
{
if (this.ControllerContext.RouteData.Values["errorinfo"] != null)
{
HandleErrorInfo errorinfo = null;
errorinfo = (HandleErrorInfo)this.ControllerContext.RouteData.Values["errorinfo"];
_httplog.Error(1, Enums.ErrorCode.INTERNAL_SERVER_ERROR, "A global application exception was handled and the end user was redirected to the Error Controller with Http500 Action.", errorinfo.Exception, true);
}
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
// explicitly set the View below
return View("Unavailable");
}
public ActionResult Unavailable()
{
if (this.ControllerContext.RouteData.Values["errorinfo"] != null)
{
HandleErrorInfo errorinfo = null;
errorinfo = (HandleErrorInfo)this.ControllerContext.RouteData.Values["errorinfo"];
_httplog.Error(1, Enums.ErrorCode.INTERNAL_SERVER_ERROR, "A global application exception was handled and the end user was redirected to the Error Controller with Unavailable Action.", errorinfo.Exception, true);
}
Response.StatusCode = (int)HttpStatusCode.OK;
// explicitly set the View below
return View("Unavailable");
}
}
I added add'l logging in each method to try to find the culprit but to no avail. What's odd is that, thru adding extra logging, the global error handler is NOT firing when this exception is thrown and logged!
Has anyone encountered something like this?
Try to add this line in your web.config
<customErrors mode="On" />
Added some more logging and found the culprit. In the Error controller, Http404 action, I added the following logging block
_httplog.Info(String.Format("Controller is {0}, Action is {1} and URL is {2}.",
RouteData.Values["controller"].ToString(),
RouteData.Values["action"].ToString(),
RouteData.Values["url"].ToString()));
The resulting log showed:
Message: Controller is Error, Action is Http404 and URL is __utm.gif.
This is a Google Analytics image file and I had seen this gif call in my server dumps during performance monitoring, however, didn't correlate it with my Error controller firing. Since my site is a subsite and the parent site uses GA, the urchin script was enabled in the DEV env.
Here's why it fired - in the RouteConfig.cs, the very last entry handles all bad URLs and routes them to the Error controller, Http404 action, like so:
routes.MapRoute("BadRoute", "{*url}", new { controller = "Error", action = "Http404" });
This is to handle any bad URL path and keep the current URL in place - instead of showing an IIS 404 or a customerror html page.
This answers the question as to why my Error controller got called outside of the code, however does not answer why the MVC framework went looking for a different error controller with a matching default view. The fix for that entailed disabling the following line in my FilterConfig.cs
filters.Add(new HandleErrorAttribute());
This was firing despite the app handling errors in the global.asax. Hope this helps!
I've made some modifications to Global.asax so that I can show custom error pages (403, 404, and 500) Here's the code:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
//FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
protected void Application_Error(object sender, EventArgs e)
{
if (Context.IsCustomErrorEnabled)
{
ShowCustomErrorPage(Server.GetLastError());
}
}
private void ShowCustomErrorPage(Exception exception)
{
HttpException httpException = exception as HttpException;
if (httpException == null)
{
httpException = new HttpException(500, "Internal Server Error", exception);
}
Response.Clear();
RouteData routeData = new RouteData();
routeData.Values.Add("controller", "Error");
routeData.Values.Add("fromAppErrorEvent", true);
switch (httpException.GetHttpCode())
{
case 403:
routeData.Values.Add("action", "AccessDenied");
break;
case 404:
routeData.Values.Add("action", "NotFound");
break;
case 500:
routeData.Values.Add("action", "ServerError");
break;
default:
routeData.Values.Add("action", "DefaultError");
routeData.Values.Add("httpStatusCode", httpException.GetHttpCode());
break;
}
Server.ClearError();
IController controller = new ErrorController();
controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
}
}
I've also added the following to my Web.Config:
<customErrors mode="On">
<!-- There is custom handling of errors in Global.asax -->
</customErrors>
The custom error pages show up correctly, and ELMAH will correctly log the error that was (purposefully) thrown. But ELMAH also catches and logs an additional error:
System.InvalidOperationException: The view 'Error' or its master was not found or no view engine supports the searched locations. The following locations were searched: ~/Views/account/Error.aspx ~/Views/account/Error.ascx ~/Views/Shared/Error.aspx ~/Views/Shared/Error.ascx ~/Views/account/Error.cshtml ~/Views/account/Error.vbhtml ~/Views/Shared/Error.cshtml ~/Views/Shared/Error.vbhtml
My first instincts led me to disabling the global HandleErrorAttribute in the filter configuration. And, similar SO questions such as:MVC problem with custom error pages led me to believe my suspicions were right. But even after disabling the global HandleErrorAttribute I am still getting the Error that the Error view could not be found! What gives? My only other hunch is that my base controller derives from System.Web.Mvc.Controller I tried to examine the source to see if the HandleErrorAttribute is applied to System.Web.Mvc.Controller but couldn't glean anything...
UPDATE:
I tried overriding my base controller to mark exceptions as handled like this:
protected override void OnException(ExceptionContext filterContext)
{
filterContext.ExceptionHandled = true;
base.OnException(filterContext);
}
but that didn't solve the problem.
UPDATE2:
I placed an Error.aspx file into the shared views, just to see what would happen. When it's there, ELMAH logs the forced exception, and then the shared view is served up - it never reaches Application_Error() .... not too sure what to make of it.
Finally got it working to my satisfaction...
The Elmah.Mvc package applies a "hidden" error handler. I've disabled this by adding the following line in web.config <appSettings> (the value was set to "false" by default from nuget install)
<add key="elmah.mvc.disableHandleErrorFilter" value="true" />
So, now my errors propagate up to Application_Error and are logged by Elmah, bypassing the Elmah filter, and display the proper error page (not the one in /shared/error.cshtml)
If you are running in IIS 7 integrated mode, you will need to add Response.TrySkipIisCustomErrors = true; in Application_Error. Otherwise IIS will still redirect the client to a custom error page, despite anything you do in code.
See here for additional details: http://www.west-wind.com/weblog/posts/2009/Apr/29/IIS-7-Error-Pages-taking-over-500-Errors
Edit: here's the body of my Application_Error:
if (HttpContext.Current != null)
{
Server.ClearError();
Response.TrySkipIisCustomErrors = true;
RouteData data = new RouteData();
data.Values.Add("controller", "Error");
data.Values.Add("action", "Error");
IController controller = new MyApp.Controllers.ErrorController();
controller.Execute(new RequestContext(new HttpContextWrapper(Context), data));
}