Как получить доступ к HTTP StatusDescription на странице пользовательских ошибок

Когда действие (asp.net mvc 5) не может найти что-то в базе данных, пользователь должен увидеть страницу с коротким настраиваемым сообщением об ошибке, например "Invoice 5 does not exist", Кроме того, ответ должен иметь 404 HTTP код

Другой пример: когда действие вызывается неправильно, пользователь должен видеть, например, "Parameter 'invoiceId' is required", Кроме того, ответ должен иметь 400 HTTP код

Я попытался сохранить пользовательские сообщения в описании статуса HTTP и отобразить их на пользовательских страницах ошибок. Есть два подхода (afaik):

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>

В

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>

В любом случае, как описание статуса HTTP может отображаться на пользовательской странице? Как получить к нему доступ в действии или представлении NotFound/BadRequest?

Если это невозможно, как ответ 4xx может содержать управляемое данными сообщение, приятно представленное пользователю?

Обновить:

Я был одержим использованием конфигурации, но кажется, что невозможно вставить пользовательскую строку в страницу ошибки, объявленную в web.config. И @Carl, и @V2Solutions ответили только на мой запасной вопрос (начиная с "Если это невозможно..."). Я бы суммировал их подходы так:

C (@Carl)

Не используйте web.config или HttpStatusCodeResult.

Бросать исключения, перехватывать их в Application_Error и программно отображать пользовательские страницы ошибок.

D (@V2Solutions)

Не используйте web.config, исключения или HttpStatusCodeResult.

Установите код состояния / описание непосредственно в ответе и верните любой вид, который вам нравится.

2 ответа

Решение

Создать ErrorController - это позволяет настроить страницы ошибок конечного пользователя и коды состояния. Каждый результат действия принимает исключение, которое вы можете добавить к данным вашего маршрута в вашем методе application_error в вашем global.asax. Это не обязательно должен быть объект исключения, это может быть что угодно - просто добавьте его к routedata в вашем 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");
    }
}

Ваша ошибка просмотра:

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

<h2>Error</h2>

@Model.Message

Добавьте маршрут для перехвата всех URL-адресов в конец конфигурации вашего маршрута - это захватывает все 404, которые еще не перехвачены при сопоставлении существующих маршрутов:

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

В вашем 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));
        }
    }

Потом когда бросаешь

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

Ваше сообщение будет передано и показано пользователю. На этом этапе вы можете указать, какой код состояния вы хотите использовать, и расширить оператор switch в application_error.

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);
            }
        }
    }
}

Чтобы использовать в вашем действии, как это:

public ActionResult Index()
{
    // Some processing
    if (...)
        return HttpNotFound();
    // Other processing
}
Другие вопросы по тегам