However the previous strategies do not handle a request to a Bad/Unknown controller. For example, I do not have a "/IDoNotExist", if I request this I get the generic 404 page from the web server and not my 404 if I use routing + override.

So finally, my question is: Is there any way to catch this type of request using a route or something else in the MVC framework itself?

OR should I just default to using Web.Config customErrors as my 404 handler and forget all this? I assume if I go with customErrors I'll have to store the generic 404 page outside of /Views due to the Web.Config restrictions on direct access. Anyway any best practices or guidance is appreciated.

it's 404 error, i would just not bother about it. let it display 404. as definitely user mistyped something. or if it is something thats moved then your application should take that request and do redirect permanent. 404 belongs to webserver not application. you can always customize iis pages for error.
–
mamuAug 1 '10 at 4:08

It's a shame that 4 stable releases later and more than 5 years on, the situation for handling 404s in asp.net MVC + IIS hasn't really improved and this is still the go to Q&A for how to handle it.
–
joelmdevAug 29 '14 at 13:50

update: checking for an http 404 is definitely required, but i'm still not quite sure when you'd ever get a 500. also you need to also explicitly set the Response.StatusCode = 404 or 500 otherwise google will start indexing these pages if you are returning a 200 status code which this code currently does
–
Simon_WeaverAug 11 '09 at 8:03

There is an underlying flaw with this whole suggestion - by the time execution has bubbled up to the Global.asax, too much of the HttpContext is missing. You can not route back into your controllers like the example suggests. Refer to the comments in the blog link at top.
–
cottsakMay 16 '10 at 5:56

3

As some of the comments from above and the linked post mention, this doesn't appear to work. The errorcontroller is hit, but a blank screen returns. (using mvc 3)
–
RyanWJan 21 '11 at 16:14

4

something doesn't feel right, the whole purpose of MVC is to remove all that abstraction and yet here it is again...
–
AlexanderNFeb 6 '11 at 4:22

I want a way to handle in the same manner as above, custom 404s - like when an ID is submitted for an object that does not exist (maybe deleted)

I want all my 404s to return an MVC view (not a static page) to which i can pump more data later if necessary (good 404 designs) and they must return the HTTP 404 status code

Solution

I think you should save Application_Error in the Global.asax for higher things, like unhandled exceptions and logging (like Shay Jacoby's answer shows) but not 404 handling. This is why my suggestion keeps the 404 stuff out of the Global.asax file.

Step 1: Have a common place for 404-error logic

This is a good idea for maintainability. Use an ErrorController so that future improvements to your well designed 404 page can adapt easily. Also, make sure your response has the 404 code!

I think its better to catch errors closer to where they originate. This is why i prefer the above to the Application_Error handler.

This is the second place to catch 404s.

Step 4: Add a NotFound route to Global.asax for urls that fail to be parsed into your app

This route should point to our Http404 action. Notice the url param will be a relative url because the routing engine is stripping the domain part here? That is why we have all that conditional url logic in Step 1.

This is the third and final place to catch 404s in an MVC app that you don't invoke yourself. If you don't catch unmatched routes here then MVC will pass the problem up to ASP.NET (Global.asax) and you don't really want that in this situation.

Step 5: Finally, invoke 404s when your app can't find something

Like when a bad ID is submitted to my Loans controller (derives from MyController):

Thanks for the full write-up. One addition is that when running under IIS7 you need to add set the property "TrySkipIisCustomErrors" to true. Otherwise IIS will still return the default 404 page. We added Response.TrySkipIiisCustomErrors=true; after the line in Step 5 that sets the status code. msdn.microsoft.com/en-us/library/…
–
RickJun 16 '10 at 15:17

1

@Ryan The customErrors section of the web.config defines static redirect pages that are handled at a high level in aspnet if not IIS. This is not what i wanted as i needed to have the result be MVC Views (so i can have data in them etc). I wouldn't say categorically that "customErrors is obsolete in MVC" but for me and this 404 solution they certainly are.
–
cottsakJan 31 '11 at 1:06

7

Anyone care to update this for MVC3?
–
David MurdochFeb 25 '11 at 15:20

7

This works fine for MVC3. I switched ObjectFactory.GetInstance to MVC3's DependencyResolver.Current.GetService instead so it's more generic. I am using Ninject.
–
subkamranMar 20 '11 at 23:22

81

Does anyone else find it patently insane that such a common thing as 404's in a web framework is so bloody complicated.
–
qesNov 7 '12 at 17:14

Sample Project

THIS SHOULD HAVE BEEN THE ACCEPTED ANSWER!!! Works excellent on ASP.NET MVC 3 with IIS Express.
–
Andrei RîneaMay 4 '11 at 22:23

6

If you're using IIS7+ this is definitely the way to go. +1!
–
elo80kaMay 16 '11 at 1:10

3

is it possible to return only a status 404 when youre working in JSON within the same project?
–
VinnyGAug 18 '11 at 0:06

3

This is working great in iis express, but as soon as i deploy the site in production IIS 7.5, all i get is a white page instead of the error view.
–
MouldeMar 9 '12 at 21:01

2

According to my tests (with MVC3) this breaks customErrors mode="On" together with the HandleErrorAttribute to be functional. Custom error pages for unhandled exceptions in controller actions are not served anymore.
–
SlaumaOct 24 '12 at 17:41

Now take careful note of the ROUTES I've decided to use. You can use anything, but my routes are

/NotFound <- for a 404 not found, error page.

/ServerError <- for any other error, include errors that happen in my code. this is a 500 Internal Server Error

See how the first section in <system.web> only has one custom entry? The statusCode="404" entry? I've only listed one status code because all other errors, including the 500 Server Error (ie. those pesky error that happens when your code has a bug and crashes the user's request) .. all the other errors are handled by the setting defaultRedirect="/ServerError" .. which says, if you are not a 404 page not found, then please goto the route /ServerError.

That lists two ignore routes -> axd's and favicons (ooo! bonus ignore route, for you!)
Then (and the order is IMPERATIVE HERE), I have my two explicit error handling routes .. followed by any other routes. In this case, the default one. Of course, I have more, but that's special to my web site. Just make sure the error routes are at the top of the list. Order is imperative.

Finally, while we are inside our global.asax file, we do NOT globally register the HandleError attribute. No, no, no sir. Nadda. Nope. Nien. Negative. Noooooooooo...

Ok, lets check this out. First of all, there is NO[HandleError] attribute here. Why? Because the built in ASP.NET framework is already handling errors AND we have specified all the shit we need to do to handle an error :) It's in this method!

Next, I have the two action methods. Nothing tough there. If u wish to show any exception info, then u can use Server.GetLastError() to get that info.

Bonus WTF: Yes, I made a third action method, to test error handling.

Step 4 - Create the Views

And finally, create two views. Put em in the normal view spot, for this controller.

Bonus comments

You don't need an Application_Error(object sender, EventArgs e)

The above steps all work 100% perfectly with Elmah. Elmah fraking wroxs!

So I tried to implement this but a couple problems... first, You need a ~ before the path in weeb.config or it doesnt work for virtual directories. 2-If IIS custom errors fires and the view is using a layout it doesn't render at all, just a white page. I solved that by adding this line into the controller "Response.TrySkipIisCustomErrors = true;" . However, it still doesn't work if you go to a url that is a file but 404.. like mysite/whatever/fake.html gets a white page.
–
Robert NoackJun 21 '14 at 17:22

-1, sorry, for me any solution that does change url for 404 is wrong. and with webconfig there is no way in MVC that you can handle it without changing url, or you need to create static html files or aspx (yes, plain old aspx files) to be able to do it. your solution is fine if you like ?aspxerrorpath=/er/not/found to have in urls.
–
GutekAug 21 '14 at 12:39

3

This might sounds really weird - but my answer was provided ages ago and I agree with your @Gutek, I don't like doing a redirect to an error page anymore. I used to (ref to my answer :P). If the error occured on /some/resource .. then THAT resource should return a 404 or 500, etc. MASSIVE SEO implications otherwise. Ahh .. how times change :)
–
Pure.KromeAug 22 '14 at 8:34

Great answer. Worthy of many more upvotes. Why does your global.asax code not work/belong in Application_Error.
–
NinjaNyeApr 27 '12 at 9:35

7

Thanks! It cannot be done under Application_Error, because explicit 404s thrown from a controller are not considered errors on ASP.NET. If you return a HttpNotFound() from a controller, the Application_Error event will never trigger.
–
MarcoMay 7 '12 at 15:44

1

I think you forgot public ActionResult NotFound() {} in your ErrorsController. Also, can you explain how your _NotFound partial would look like for AJAX requests?
–
d4n3May 9 '12 at 10:00

1

With MVC 4 I always get MissingMethodException: Cannot create an abstract class on the line c.Execute(new RequestContext(new HttpContextWrapper(Context), rd)); Any ideas?
–
µBioNov 12 '12 at 17:19

1

If the "not found" URL includes a dot in the path (e.g. example.com/hi.bob) then the Application_EndRequest doesn't fire at all, and I get IE's generic 404 page.
–
Bob.at.SBSJul 14 '14 at 2:14

I think the key was to modify the existing request rather than make a new one, but I did this a little while ago so I'm not sure that was it. The "InvokeHttp404" didn't work from the controller factory.
–
Dave KNov 1 '10 at 18:27

I've updated my answer today with some MVC2 specifics. Can you please tell me if my solution as detailed above still does not work for you?
–
cottsakFeb 16 '11 at 0:29

Here is another method using MVC tools which you can handle requests to bad controller names, bad route names, and any other criteria you see fit inside of an Action method. Personally, I prefer to avoid as many web.config settings as possible, because they do the 302 / 200 redirect and do not support ResponseRewrite (Server.Transfer) using Razor views. I'd prefer to return a 404 with a custom error page for SEO reasons.

Usage

Step 1

Add the following setting to your web.config. This is required to use MVC's HandleErrorAttribute.

<customErrors mode="On" redirectMode="ResponseRedirect" />

Step 2

Add a custom HandleHttpErrorAttribute similar to the MVC framework's HandleErrorAttribute, except for HTTP errors:

<AttributeUsage(AttributeTargets.All, AllowMultiple:=True)>
Public Class HandleHttpErrorAttribute
Inherits FilterAttribute
Implements IExceptionFilter
Private Const m_DefaultViewFormat As String = "ErrorHttp{0}"
Private m_HttpCode As HttpStatusCode
Private m_Master As String
Private m_View As String
Public Property HttpCode As HttpStatusCode
Get
If m_HttpCode = 0 Then
Return HttpStatusCode.NotFound
End If
Return m_HttpCode
End Get
Set(value As HttpStatusCode)
m_HttpCode = value
End Set
End Property
Public Property Master As String
Get
Return If(m_Master, String.Empty)
End Get
Set(value As String)
m_Master = value
End Set
End Property
Public Property View As String
Get
If String.IsNullOrEmpty(m_View) Then
Return String.Format(m_DefaultViewFormat, Me.HttpCode)
End If
Return m_View
End Get
Set(value As String)
m_View = value
End Set
End Property
Public Sub OnException(filterContext As System.Web.Mvc.ExceptionContext) Implements System.Web.Mvc.IExceptionFilter.OnException
If filterContext Is Nothing Then Throw New ArgumentException("filterContext")
If filterContext.IsChildAction Then
Return
End If
If filterContext.ExceptionHandled OrElse Not filterContext.HttpContext.IsCustomErrorEnabled Then
Return
End If
Dim ex As HttpException = TryCast(filterContext.Exception, HttpException)
If ex Is Nothing OrElse ex.GetHttpCode = HttpStatusCode.InternalServerError Then
Return
End If
If ex.GetHttpCode <> Me.HttpCode Then
Return
End If
Dim controllerName As String = filterContext.RouteData.Values("controller")
Dim actionName As String = filterContext.RouteData.Values("action")
Dim model As New HandleErrorInfo(filterContext.Exception, controllerName, actionName)
filterContext.Result = New ViewResult With {
.ViewName = Me.View,
.MasterName = Me.Master,
.ViewData = New ViewDataDictionary(Of HandleErrorInfo)(model),
.TempData = filterContext.Controller.TempData
}
filterContext.ExceptionHandled = True
filterContext.HttpContext.Response.Clear()
filterContext.HttpContext.Response.StatusCode = Me.HttpCode
filterContext.HttpContext.Response.TrySkipIisCustomErrors = True
End Sub
End Class

Step 3

Add Filters to the GlobalFilterCollection (GlobalFilters.Filters) in Global.asax. This example will route all InternalServerError (500) errors to the Error shared view (Views/Shared/Error.vbhtml). NotFound (404) errors will be sent to ErrorHttp404.vbhtml in the shared views as well. I've added a 401 error here to show you how this can be extended for additional HTTP error codes. Note that these must be shared views, and they all use the System.Web.Mvc.HandleErrorInfo object as a the model.

Step 4

Create a base controller class and inherit from it in your controllers. This step allows us to handle unknown action names and raise the HTTP 404 error to our HandleHttpErrorAttribute.

Public Class BaseController
Inherits System.Web.Mvc.Controller
Protected Overrides Sub HandleUnknownAction(actionName As String)
Me.ActionInvoker.InvokeAction(Me.ControllerContext, "Unknown")
End Sub
Public Function Unknown() As ActionResult
Throw New HttpException(HttpStatusCode.NotFound, "The specified controller or action does not exist.")
Return New EmptyResult
End Function
End Class

Step 5

Create a ControllerFactory override, and override it in your Global.asax file in Application_Start. This step allows us to raise the HTTP 404 exception when an invalid controller name has been specified.

Summary

This example demonstrated how one can use the MVC framework to return 404 Http Error Codes to the browser without a redirect using filter attributes and shared error views. It also demonstrates showing the same custom error page when invalid controller names and action names are specified.

I'll add a screenshot of an invalid controller name, action name, and a custom 404 raised from the Home/TriggerNotFound action if I get enough votes to post one =). Fiddler returns a 404 message when I access the following URLs using this solution:

Hmm, I couldn't get this to work: The IControllerFactory 'aaa.bbb.CustomControllerFactory' did not return a controller for the name '123'. - any ideas why I would get that?
–
enashnashAug 16 '12 at 0:45

redirectMode="ResponseRedirect". This will return a 302 Found + a 200 OK which is not good for SEO!
–
PussInBootsJan 18 '14 at 13:39

I prefer this answer over the others for it's simplicity and the fact that apparently some folks at Microsoft were consulted. I got three questions however and if they can be answered then I will call this answer the holy grail of all 404/500 error answers on the interwebs for an ASP.NET MVC (x) app.

@Pure.Krome

Can you update your answer with the SEO stuff from the comments pointed out by GWB (there was never any mentioning of this in your answer) - <customErrors mode="On" redirectMode="ResponseRewrite"> and <httpErrors errorMode="Custom" existingResponse="Replace">?

Can you ask your ASP.NET team friends if it is okay to do it like that - would be nice to have some confirmation - maybe it's a big no-no to change redirectMode and existingResponse in this way to be able to play nicely with SEO?!

Can you add some clarification surrounding all that stuff (customErrors redirectMode="ResponseRewrite", customErrors redirectMode="ResponseRedirect", httpErrors errorMode="Custom" existingResponse="Replace", REMOVE customErrors COMPLETELY as someone suggested) after talking to your friends at Microsoft?

As I was saying; it would be supernice if we could make your answer more complete as this seem to be a fairly popular question with 54 000+ views.

Update: Unicorn answer does a 302 Found and a 200 OK and cannot be changed to only return 404 using a route. It has to be a physical file which is not very MVC:ish. So moving on to another solution. Too bad because this seemed to be the ultimate MVC:ish answer this far.