42

Is there a way to return the same view every time a HttpNotFoundResult is returned from a controller? How do you specify this view? I'm guessing configuring a 404 page in the web.config might work, but I wanted to know if there was a better way to handle this result.

Edit / Follow up:

I ended up using the solution found in the second answer to this question with some slight tweaks for ASP.Net MVC 3 to handle my 404s: How can I properly handle 404s in ASP.Net MVC?

Community
  • 1
  • 1
Dave Brace
  • 1,809
  • 2
  • 16
  • 20

6 Answers6

65

HttpNotFoundResult doesn't render a view. It simply sets the status code to 404 and returns an empty result which is useful for things like AJAX but if you want a custom 404 error page you could throw new HttpException(404, "Not found") which will automatically render the configured view in web.config:

<customErrors mode="RemoteOnly" redirectMode="ResponseRewrite">
   <error statusCode="404" redirect="/Http404.html" />
</customErrors>
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 2
    If that's the case... what purpose does returning a HttpNotFound() serve? – Dave Brace Feb 13 '11 at 17:12
  • 1
    Slightly unrelated, but is that how you recommend handling the case where someone tries to go to a page with a bad id? For instance, a bad question id on stackoverflow? The 404 HttpException? – Dave Brace Feb 13 '11 at 17:27
  • @Rifk, that's one way of doing it. You could also try configuring custom error pages in IIS for different status codes and then use `HttpNotFoundResult`. – Darin Dimitrov Feb 13 '11 at 17:29
  • @Rifk The purpose is: Let's say Site admin has deleted the user but it is still indexed by search engines. So in that condition you can return 'HttpNotFound' from the user profile page, so that search engine will update their results. – Adeel Feb 13 '11 at 17:30
  • Thanks for the responses. I just don't like the idea of returning an empty 404 page. I'd prefer to return a page with some feedback for the user, which is what lead to this question. Although I can understand the benefits for returning an empty response for AJAX calls. – Dave Brace Feb 13 '11 at 17:34
  • 1
    What about custom error pages? Is not possible to define an "error" controller and create associated Views for each kind of error? (ex. 404, 500, etc)??? – João Louros Mar 07 '11 at 23:11
  • Odd, I haven't configured anything (except for using the project wizard) and when I return HttpNotFoundResult I get a HTML page and I don't want to (since this is a RESTful request) and I can't figure out how to turn it off... – chrish Aug 04 '11 at 13:00
17

This solution combines IResultFilter and IExceptionFilter to catch either thrown HttpException or returned HttpStatusCodeResult from within an action.

public class CustomViewForHttpStatusResultFilter: IResultFilter, IExceptionFilter
{
    string viewName;
    int statusCode;

    public CustomViewForHttpStatusResultFilter(HttpStatusCodeResult prototype, string viewName)
        : this(prototype.StatusCode, viewName) {
    }

    public CustomViewForHttpStatusResultFilter(int statusCode, string viewName) {
        this.viewName = viewName;
        this.statusCode = statusCode;
    }

    public void OnResultExecuted(ResultExecutedContext filterContext) {
        HttpStatusCodeResult httpStatusCodeResult = filterContext.Result as HttpStatusCodeResult;

        if (httpStatusCodeResult != null && httpStatusCodeResult.StatusCode == statusCode) {
            ExecuteCustomViewResult(filterContext.Controller.ControllerContext);

        }
    }

    public void OnResultExecuting(ResultExecutingContext filterContext) {
    }

    public void OnException(ExceptionContext filterContext) {
        HttpException httpException = filterContext.Exception as HttpException;

        if (httpException != null && httpException.GetHttpCode() == statusCode) {
            ExecuteCustomViewResult(filterContext.Controller.ControllerContext);
            // This causes ELMAH not to log exceptions, so commented out
            //filterContext.ExceptionHandled = true;
        }
    }

    void ExecuteCustomViewResult(ControllerContext controllerContext) {
        ViewResult viewResult = new ViewResult();
        viewResult.ViewName = viewName;
        viewResult.ViewData = controllerContext.Controller.ViewData;
        viewResult.TempData = controllerContext.Controller.TempData;
        viewResult.ExecuteResult(controllerContext);
        controllerContext.HttpContext.Response.TrySkipIisCustomErrors = true;            
    }
}

You can register this filter so, specifying either the http status code of the HttpException or the concrete HttpStatusCodeResult for which you want to display the custom view.

GlobalFilters.Filters.Add(new CustomViewForHttpStatusResultFilter(new HttpNotFoundResult(), "Error404"));
// alternate syntax
GlobalFilters.Filters.Add(new CustomViewForHttpStatusResultFilter(404, "Error404"));

It handles exceptions and HttpStatusCodeResult thrown or returned within an action. It won't handle errors that occur before MVC selects a suitable action and controller like this common problems:

  • Unknown routes
  • Unknown controllers
  • Unknown actions

For handling these types of NotFound errors, combine this solution with other solutions to be found in stackoverflow.

dampee
  • 3,392
  • 1
  • 21
  • 37
Germán
  • 1,183
  • 8
  • 11
  • 1
    I combined the solution offered here with http://stackoverflow.com/a/27354140/2310818 to take care of all sorts of errors (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 10:00
15

Useful info from @Darin Dimitrov that HttpNotFoundResult is actually returning empty result.

After some study. The workaround for MVC 3 here is to derive all HttpNotFoundResult, HttpUnauthorizedResult, HttpStatusCodeResult classes and implement new (overriding it) HttpNotFound() method in BaseController.

It is best practise to use base Controller so you have 'control' over all derived Controllers.

I create new HttpStatusCodeResult class, not to derive from ActionResult but from ViewResult to render the view or any View you want by specifying the ViewName property. I follow the original HttpStatusCodeResult to set the HttpContext.Response.StatusCode and HttpContext.Response.StatusDescription but then base.ExecuteResult(context) will render the suitable view because again I derive from ViewResult. Simple enough is it? Hope this will be implemented in the MVC core.

See my BaseController bellow:

using System.Web;
using System.Web.Mvc;

namespace YourNamespace.Controllers
{
    public class BaseController : Controller
    {
        public BaseController()
        {
            ViewBag.MetaDescription = Settings.metaDescription;
            ViewBag.MetaKeywords = Settings.metaKeywords;
        }

        protected new HttpNotFoundResult HttpNotFound(string statusDescription = null)
        {
            return new HttpNotFoundResult(statusDescription);
        }

        protected HttpUnauthorizedResult HttpUnauthorized(string statusDescription = null)
        {
            return new HttpUnauthorizedResult(statusDescription);
        }

        protected class HttpNotFoundResult : HttpStatusCodeResult
        {
            public HttpNotFoundResult() : this(null) { }

            public HttpNotFoundResult(string statusDescription) : base(404, statusDescription) { }

        }

        protected class HttpUnauthorizedResult : HttpStatusCodeResult
        {
            public HttpUnauthorizedResult(string statusDescription) : base(401, statusDescription) { }
        }

        protected class HttpStatusCodeResult : ViewResult
        {
            public int StatusCode { get; private set; }
            public string StatusDescription { get; private set; }

            public HttpStatusCodeResult(int statusCode) : this(statusCode, null) { }

            public HttpStatusCodeResult(int statusCode, string statusDescription)
            {
                this.StatusCode = statusCode;
                this.StatusDescription = statusDescription;
            }

            public override void ExecuteResult(ControllerContext context)
            {
                if (context == null)
                {
                    throw new ArgumentNullException("context");
                }

                context.HttpContext.Response.StatusCode = this.StatusCode;
                if (this.StatusDescription != null)
                {
                    context.HttpContext.Response.StatusDescription = this.StatusDescription;
                }
                // 1. Uncomment this to use the existing Error.ascx / Error.cshtml to view as an error or
                // 2. Uncomment this and change to any custom view and set the name here or simply
                // 3. (Recommended) Let it commented and the ViewName will be the current controller view action and on your view (or layout view even better) show the @ViewBag.Message to produce an inline message that tell the Not Found or Unauthorized
                //this.ViewName = "Error";
                this.ViewBag.Message = context.HttpContext.Response.StatusDescription;
                base.ExecuteResult(context);
            }
        }
    }
}

To use in your action like this:

public ActionResult Index()
{
    // Some processing
    if (...)
        return HttpNotFound();
    // Other processing
}

And in _Layout.cshtml (like master page)

<div class="content">
    @if (ViewBag.Message != null)
    {
        <div class="inlineMsg"><p>@ViewBag.Message</p></div>
    }
    @RenderBody()
</div>

Additionally you can use a custom view like Error.shtml or create new NotFound.cshtml like I commented in the code and you may define a view model for the status description and other explanations.

CallMeLaNN
  • 8,328
  • 7
  • 59
  • 74
2
protected override void HandleUnknownAction(string actionName)
{
    ViewBag.actionName  = actionName;
    View("Unknown").ExecuteResult(this.ControllerContext);
}
Adriano Carneiro
  • 57,693
  • 12
  • 90
  • 123
0

Here is true answer which allows fully customize of error page in single place. No need to modify web.confiog or create sophisticated classes and code. Works also in MVC 5.

Add this code to controller:

        if (bad) {
            Response.Clear();
            Response.TrySkipIisCustomErrors = true;
            Response.Write(product + I(" Toodet pole"));
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            //Response.ContentType = "text/html; charset=utf-8";
            Response.End();
            return null;
        }

Based on http://www.eidias.com/blog/2014/7/2/mvc-custom-error-pages

Andrus
  • 26,339
  • 60
  • 204
  • 378
0

Please follow this if you want httpnotfound Error in your controller

  public ActionResult Contact()
    {

        return HttpNotFound();
    }