Shouldn't [RequireHttps] in MVC do a 301 permanent redirect? Why does it do a 302 (bad for SEO?)

Dulan's answer is close, but it does not work, at least with our MVC 4+ solution. But after some trial and error, we did get ours working with 301's instead of 302's. Here is the new class:

public class CustomRequireHttpsAttribute : RequireHttpsAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        #if !DEBUG
        base.OnAuthorization(filterContext);

        if (!filterContext.HttpContext.Request.IsSecureConnection)
        {
            string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
            filterContext.Result = new RedirectResult(url, true);
        }
        #endif
    }
}

The reason Dulan's answer didn't work seems to be because the Permanent property of the filterContext.Result is readonly and can only be set when the RedirectResult() is called, and the problem is that RedirectResult() is called in the base.OnAuthorization() method. So just call the base method, then override the filterContext.Result below with the second parameter of true to make the result Permanent. After doing this, we began to see 301 codes in Fiddler2.


It seems as if the choice to go with 302 over 301 was a little arbitrary to begin with. However, it does not necessarily follow that every URL is going to "have" to utilize the HTTPS scheme. There very well could be a page that allows access from both HTTP or HTTPS even if it may encourage the latter. An implementation where this may occur could have some code wired up to determine whether or not to use HTTPS based on some special criteria.

As a case scenario, take a look at Gmail. Within the settings, one is capable of allowing or disallowing the HTTPS protocol throughout large portions of the application. Which code should be returned then? 301 wouldn't be accurate, as it isn't "permanent"...only a change at the behest of the user. Sadly, 302 isn't quite accurate either because a 302 error implies that there is intent to change the link back at some point in the future (related reference http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html).

Granted, Gmail is a rough example because the portions of the site that allow that option are not typically indexed by a search engine, but the possibility still exists.

And to answer your final question, if you want a different status code in ASP.NET MVC (which I assume you're using from the small syntax example), it is possible to change with a simple, custom attribute:

public class MyRequireHttpsAttribute : RequireHttpsAttribute
{
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);

        if (!filterContext.HttpContext.Request.IsSecureConnection)
            filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.MovedPermanently;
    }
}

Now all actions that implement the attribute should return a 301 status code when accessed via the HTTP protocol.


Dulan's solution put me on the right path, but the code sample didn't stop the 302 redirect from the core RequireHttpsAttribute implementation. So, I looked up the code of RequireHttpsAttribute and hacked it. Here's what I came up with:

using System.Net;
using System.Web.Mvc;
using System;
using System.Diagnostics.CodeAnalysis;

[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed because type contains virtual extensibility points.")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class RequireHttps301Attribute : FilterAttribute, IAuthorizationFilter 
{

    public virtual void OnAuthorization(AuthorizationContext filterContext) 
    {
        if (filterContext == null) {
            throw new ArgumentNullException("filterContext");
        }

        if (!filterContext.HttpContext.Request.IsSecureConnection) {
            HandleNonHttpsRequest(filterContext);
        }
    }

    protected virtual void HandleNonHttpsRequest(AuthorizationContext filterContext) 
    {
        // only redirect for GET requests, otherwise the browser might not propagate the verb and request
        // body correctly.

        if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) {
            throw new InvalidOperationException("Only redirect for GET requests, otherwise the browser might not propagate the verb and request body correctly.");
        }

        // redirect to HTTPS version of page
        string url = "https://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
        //what mvc did to redirect as a 302 
        //filterContext.Result = new RedirectResult(url);

        //what I did to redirect as a 301
        filterContext.HttpContext.Response.StatusCode =  (int)HttpStatusCode.MovedPermanently;
        filterContext.HttpContext.Response.RedirectLocation = url;
    }
}