从 ASP.NET MVC 操作发送 HTTP 404 响应的正确方法是啥?

Posted

技术标签:

【中文标题】从 ASP.NET MVC 操作发送 HTTP 404 响应的正确方法是啥?【英文标题】:What is the proper way to send an HTTP 404 response from an ASP.NET MVC action?从 ASP.NET MVC 操作发送 HTTP 404 响应的正确方法是什么? 【发布时间】:2010-10-04 17:31:15 【问题描述】:

如果给定路线:

FeedName/ItemPermalink

例如:/Blog/Hello-World

如果该项目不存在,我想返回 404。在 ASP.NET MVC 中执行此操作的正确方法是什么?

【问题讨论】:

感谢您提出这个问题。这将在我的标准项目添加中进行:D 【参考方案1】:

从臀部射击(牛仔编码;-)),我建议这样:

控制器:

public class HomeController : Controller

    public ActionResult Index()
    
        return new HttpNotFoundResult("This doesn't exist");
    


HttpNotFoundResult:

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

namespace YourNamespaceHere

    /// <summary>An implementation of <see cref="ActionResult" /> that throws an <see cref="HttpException" />.</summary>
    public class HttpNotFoundResult : ActionResult
    
        /// <summary>Initializes a new instance of <see cref="HttpNotFoundResult" /> with the specified <paramref name="message"/>.</summary>
        /// <param name="message"></param>
        public HttpNotFoundResult(String message)
        
            this.Message = message;
        

        /// <summary>Initializes a new instance of <see cref="HttpNotFoundResult" /> with an empty message.</summary>
        public HttpNotFoundResult()
            : this(String.Empty)  

        /// <summary>Gets or sets the message that will be passed to the thrown <see cref="HttpException" />.</summary>
        public String Message  get; set; 

        /// <summary>Overrides the base <see cref="ActionResult.ExecuteResult" /> functionality to throw an <see cref="HttpException" />.</summary>
        public override void ExecuteResult(ControllerContext context)
        
            throw new HttpException((Int32)HttpStatusCode.NotFound, this.Message);
        
    

// By Erik van Brakel, with edits from Daniel Schaffer :)

使用这种方法,您可以遵守框架标准。那里已经有一个 HttpUnauthorizedResult,所以这只会在其他开发人员眼中扩展框架,以便稍后维护您的代码(你知道,知道你住在哪里的精神病)。

你可以使用反射器看一下程序集,看看HttpUnauthorizedResult是如何实现的,因为我不知道这种方法是否遗漏了什么(似乎太简单了)。


我刚才确实用反射器看了一下HttpUnauthorizedResult。似乎他们在对 0x191 (401) 的响应中设置了 StatusCode。虽然这适用于 401,但使用 404 作为新值,我似乎在 Firefox 中得到了一个空白页。 Internet Explorer 显示默认的 404(不是 ASP.NET 版本)。使用 webdeveloper 工具栏,我检查了 FF 中的标题,它确实显示了 404 Not Found 响应。可能只是我在 FF 中配置错误。


话虽如此,我认为 Jeff 的方法是 KISS 的一个很好的例子。如果您真的不需要此示例中的冗长,那么他的方法也可以正常工作。

【讨论】:

是的,我也注意到了枚举。正如我所说,这只是一个粗略的示例,请随时对其进行改进。毕竟这应该是一个知识库;-) 我觉得我有点过火了......享受吧:D FWIW,Jeff 的示例还要求您有一个自定义 404 页面。 抛出 HttpException 而不是仅仅设置 HttpContext.Response.StatusCode = 404 的一个问题是,如果您使用 OnException Controller 处理程序(就像我一样),它也会捕获 HttpExceptions。所以我认为只设置 StatusCode 是一种更好的方法。 MVC3 中的 HttpException 或 HttpNotFoundResult 在很多方面都很有用。在@Igor Brejc 的情况下,只需在 OnException 中使用 if 语句来过滤掉 not found 错误。【参考方案2】:

我们就是这样做的;此代码在BaseController 中找到

/// <summary>
/// returns our standard page not found view
/// </summary>
protected ViewResult PageNotFound()

    Response.StatusCode = 404;
    return View("PageNotFound");

这样称呼

public ActionResult ShowUserDetails(int? id)
        
    // make sure we have a valid ID
    if (!id.HasValue) return PageNotFound();

【讨论】:

此操作是否连接到默认路由?看不到它是如何被执行的。 可以这样运行:protected override void HandleUnknownAction(string actionName) PageNotFound().ExecuteResult(this.ControllerContext); 我曾经这样做过,但发现拆分结果和显示的视图是一种更好的方法。在下面查看我的答案。【参考方案3】:
throw new HttpException(404, "Are you sure you're in the right place?");

【讨论】:

我喜欢这个,因为它遵循web.config 中设置的自定义错误页面。【参考方案4】:

HttpNotFoundResult 是我正在使用的伟大的第一步。返回一个 HttpNotFoundResult 很好。那么问题来了,接下来呢?

我创建了一个名为 HandleNotFoundAttribute 的操作过滤器,然后显示 404 错误页面。由于它返回一个视图,您可以为每个控制器创建一个特殊的 404 视图,或者让我们使用默认的共享 404 视图。这甚至会在控制器不存在指定操作时调用,因为框架会抛出状态码为 404 的 HttpException。

public class HandleNotFoundAttribute : ActionFilterAttribute, IExceptionFilter

    public void OnException(ExceptionContext filterContext)
    
        var httpException = filterContext.Exception.GetBaseException() as HttpException;
        if (httpException != null && httpException.GetHttpCode() == (int)HttpStatusCode.NotFound)
        
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; // Prevents IIS from intercepting the error and displaying its own content.
            filterContext.ExceptionHandled = true;
            filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.NotFound;
            filterContext.Result = new ViewResult
                                        
                                            ViewName = "404",
                                            ViewData = filterContext.Controller.ViewData,
                                            TempData = filterContext.Controller.TempData
                                        ;
        
    

【讨论】:

【参考方案5】:

请注意,从 MVC3 开始,您可以只使用 HttpStatusCodeResult

【讨论】:

或者,更简单,HttpNotFoundResult【参考方案6】:

使用 ActionFilter 难以维护,因为每当我们抛出错误时,都需要在属性中设置过滤器。如果我们忘记设置它怎么办?一种方法是在基本控制器上派生OnException。您需要定义一个派生自ControllerBaseController,并且您的所有控制器都必须派生自BaseController。最好有一个基本控制器。

注意如果使用Exception,响应状态码是500,所以我们需要将其更改为404表示未找到,401表示未授权。就像我上面提到的,在BaseController 上使用OnException 覆盖以避免使用过滤器属性。

新的 MVC 3 还通过向浏览器返回一个空视图来增加麻烦。经过一些研究后的最佳解决方案是基于我在这里的回答How to return a view for HttpNotFound() in ASP.Net MVC 3?

为了更方便,我把它贴在这里:


经过一番研究。 MVC 3 的解决方法是派生所有 HttpNotFoundResultHttpUnauthorizedResultHttpStatusCodeResult 类并在BaseController.

最佳实践是使用基本控制器,这样您就可以“控制”所有派生控制器。

我创建了新的HttpStatusCodeResult 类,不是从ActionResult 派生,而是从ViewResult 派生,以通过指定ViewName 属性来呈现视图或您想要的任何View。我按照原来的HttpStatusCodeResult 设置HttpContext.Response.StatusCodeHttpContext.Response.StatusDescription 但随后base.ExecuteResult(context) 将呈现合适的视图,因为我再次派生自ViewResult。够简单吧?希望这将在 MVC 核心中实现。

在下面查看我的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

在 _Layout.cshtml 中(如母版页)

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

此外,您可以使用Error.shtml 之类的自定义视图或创建新的NotFound.cshtml,就像我在代码中评论的那样,您可以为状态描述和其他解释定义视图模型。

【讨论】:

你总是可以注册一个全局过滤器来击败一个基本控制器,因为你必须记住使用你的基本控制器! :) 不确定这在 MVC4 中是否仍然是一个问题。我当时的意思是其他人回答的 HandleNotFoundAttribute 过滤器。不必为每个动作应用。例如。它仅适用于具有 id 参数但不具有 Index() 动作的动作。我同意全局过滤器,不是 HandleNotFoundAttribute 而是自定义 HandleErrorAttribute。 我认为 MVC3 也有,不确定。很好的讨论,不管其他人可能会遇到答案

以上是关于从 ASP.NET MVC 操作发送 HTTP 404 响应的正确方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

从控制器事件(无客户端请求)向视图发送数据 ASP.NET MVC 4

从 ASP.NET MVC 操作中获取绝对 URL

从 JQuery 将 JSON 发布到 ASP.NET MVC 4 操作

无法使用 Jquery 将数据从视图发送到控制器 Asp.net MVC

asp.net mvc 4 贝宝集成

ASP.NET MVC 4 - ListBoxFor,在 ActionLink 中发送 selectedValue