How to access HTTP StatusDescription in custom err

2019-04-14 13:38发布

问题:

When an action (asp.net mvc 5) cannot find something in the database, the user must see a page with a short custom error message e.g. "Invoice 5 does not exist". Also, the response must have a 404 HTTP code.

Another example: when the action is improperly called, the user must see e.g. "Parameter 'invoiceId' is required". Also, the response must have a 400 HTTP code.

I tried to store the custom messages in the HTTP status description and to display them in custom error pages. There are two approaches (afaik):

A

mvc action:
return HttpNotFound("Invoice 5 does not exist"); OR
return new HttpStatusCodeResult(HttpStatusCode.BadRequest, "Parameter 'invoiceId' is required");

web.config:
<httpErrors errorMode="Custom">
  <remove statusCode="404" />
  <error statusCode="404" path="/Error/NotFound" responseMode="ExecuteURL" />
  <remove statusCode="400" />
  <error statusCode="400" path="/Error/BadRequest" responseMode="ExecuteURL" />
</httpErrors>

B

mvc action:
throw new HttpException(404, "Invoice 5 does not exist"); OR
throw new HttpException(400, "Parameter 'invoiceId' is required");

web.config:
<customErrors mode="On">
  <error statusCode="404" redirect="~/Error/NotFound" />
  <error statusCode="400" redirect="~/Error/BadRequest" />
</customErrors>

Either way, how can the HTTP status description be displayed in the custom page? How do I access it inside the NotFound/BadRequest action or view?

If this is impossible, how can a 4xx response contain a data-driven message, nicely presented to the user?

Update:

I was obsessed with using configuration but it seems impossible to push a custom string into an error page declared in web.config. Both @Carl and @V2Solutions answered only my fallback question (starting "If this is impossible..."). I would summarize their approaches like this:

C (@Carl)

Don't use web.config or HttpStatusCodeResult.

Throw exceptions, catch them in Application_Error and render custom error pages programmatically.

D (@V2Solutions)

Don't use web.config, exceptions, or HttpStatusCodeResult.

Set the status code/description directly in the Response and return whatever view you like.

回答1:

Create an ErrorController - this allows you to tailor your end-user error pages and status codes. Each action result accepts an exception which you can add to your route data in your application_error method in your global.asax. It doesn't have to be the exception object, it can be anything you like - just add it to the routedata in your application_error.

[AllowAnonymous]
public class ErrorController : Controller
{
    public ActionResult PageNotFound(Exception ex)
    {
        Response.StatusCode = 404;
        return View("Error", ex);
    }

    public ActionResult ServerError(Exception ex)
    {
        Response.StatusCode = 500;
        return View("Error", ex);
    }

    public ActionResult UnauthorisedRequest(Exception ex)
    {
        Response.StatusCode = 403;
        return View("Error", ex);
    }

    //Any other errors you want to specifically handle here.

    public ActionResult CatchAllUrls()
    {
        //throwing an exception here pushes the error through the Application_Error method for centralised handling/logging
        throw new HttpException(404, "The requested url " + Request.Url.ToString() + " was not found");
    }
}

Your Error View:

@model Exception
@{
    ViewBag.Title = "Error";
}

<h2>Error</h2>

@Model.Message

Add a route to catch all urls to the end of your route config - this captures all 404's that are not already caught by matching existing routes:

routes.MapRoute("CatchAllUrls", "{*url}", new { controller = "Error", action = "CatchAllUrls" });

In your global.asax:

protected void Application_Error(object sender, EventArgs e)
    {
        Exception exception = Server.GetLastError();

        //Error logging omitted

        HttpException httpException = exception as HttpException;
        RouteData routeData = new RouteData();
        IController errorController = new Controllers.ErrorController();
        routeData.Values.Add("controller", "Error");
        routeData.Values.Add("area", "");
        routeData.Values.Add("ex", exception);

        if (httpException != null)
        {
            //this is a basic example of how you can choose to handle your errors based on http status codes.
            switch (httpException.GetHttpCode())
            {
                case 404:
                    Response.Clear();

                    // page not found
                    routeData.Values.Add("action", "PageNotFound");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));

                    break;
                case 500:
                    // server error
                    routeData.Values.Add("action", "ServerError");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                    break;
                 case 403:
                    // server error
                    routeData.Values.Add("action", "UnauthorisedRequest");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                    break;
                 //add cases for other http errors you want to handle, otherwise HTTP500 will be returned as the default.
                default:
                    // server error
                    routeData.Values.Add("action", "ServerError");

                    Server.ClearError();
                    // Call the controller with the route
                    errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
                    break;
            }
        }
        //All other exceptions should result in a 500 error as they are issues with unhandled exceptions in the code
        else
        {
            routeData.Values.Add("action", "ServerError");
            Server.ClearError();
            // Call the controller with the route
            errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
        }
    }

Then when you throw

throw new HttpException(404, "Invoice 5 does not exist");

your message will be carried through and displayed to the user. You can specify at this point which status code you want to use, and extend the switch statement in the application_error.



回答2:

BaseController :

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
}