32
votes

I am trying to make sure that a certain page is never cached, and never shown when the user clicks the back button. This very highly rated answer (currently 1068 upvotes) says to use:

Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
Response.AppendHeader("Pragma", "no-cache");
Response.AppendHeader("Expires", "0");

However in IIS7 / ASP.NET MVC, when I send those headers then the client sees these response headers instead:

Cache-control: private, s-maxage=0 // that's not what I set them to
Pragma: no-cache
Expires: 0

What happened to the cache-control header? Does something native to IIS7 or ASP.NET overwrite it? I have checked my solution and I have no code that overwrites this header.

When I add Response.Headers.Remove("Cache-Control"); first, it makes no difference:

Response.Headers.Remove("Cache-Control");
Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
Response.AppendHeader("Pragma", "no-cache");
Response.AppendHeader("Expires", "0");

When I add an [OutputCache] attribute:

[OutputCache(Location = OutputCacheLocation.None)]
public ActionResult DoSomething()
{
   Response.Headers.Remove("Cache-Control");
   Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
   Response.AppendHeader("Pragma", "no-cache");
   Response.AppendHeader("Expires", "0");

   var model = DoSomething();
   return View(model);
}

Then the client response headers change to:

Cache-control: no-cache
Pragma: no-cache
Expires: 0

Which is closer, but still not the headers that I want to send. Where are these headers getting overridden and how can I stop it?

EDIT: I have checked and the incorrect headers are being sent to Chrome, FF, IE and Safari, so it looks to be a server problem not a browser related problem.

3
I can't replicate this problem in a fresh MVC3 or MVC4 application. Can you check your settings in IIS (HTTP Response Headers and Output Caching)?Rowan Freeman
In IIS7, I have no settings configured for output caching (server level or site level) and only one response header configured (X-Powered-By)JK.

3 Answers

54
votes

Through trial and error, I have found that one way to set the headers correctly for IIS7 in ASP.NET MVC is:

Response.Cache.SetCacheability(HttpCacheability.NoCache);
Response.Cache.AppendCacheExtension("no-store, must-revalidate");
Response.AppendHeader("Pragma", "no-cache");
Response.AppendHeader("Expires", "0");

The first line sets Cache-control to no-cache, and the second line adds the other attributes no-store, must-revalidate.

This may not be the only way, but does provide an alternative method if the more straightforward Response.AppendHeader("Cache-control", "no-cache, no-store, must-revalidate"); fails.

Other related IIS7 cache-control questions that may be solved by this are:

1
votes

I want to add something to JK's answer:
If you are setting the cache control to a more restrictive value than it already is, it is fine. (i.e: setting no-cache, when it is private)

But, if you want to set to a less restrictive value than it already is (i.e: setting to private, when it is no-cache), the code below will not work:

Response.Cache.SetCacheability(HttpCacheability.Private);

Because, SetCacheablitiy method has this code below and sets the cache flag only if it is more restrictive:

if (s_cacheabilityValues[(int)cacheability] < s_cacheabilityValues[(int)_cacheability]) {
    Dirtied();
   _cacheability = cacheability;
}

To overcome this in .net mvc, you need to get an instance of HttpResponseMessage and assign a CacheControlHeaderValue to its Headers.CacheControl value:

actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue
                                   {
                                       MaxAge = TimeSpan.FromSeconds(3600),
                                       Private = true
                                   };

An instance of the HttpResponseMessage is available in action filters. You can write an action filter to set cache header values like this:

public class ClientSideCacheAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        var response = actionExecutedContext.ActionContext.Response;
        response.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
        {
            MaxAge = TimeSpan.FromSeconds(9999),
            Private = true,
        };
    }
}
1
votes

If you need these headers globally in your MVC application. Add this class.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class CustomHeaders : System.Web.Mvc.ActionFilterAttribute
{
    [OutputCache(Location = System.Web.UI.OutputCacheLocation.None)]
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        context.RequestContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
        context.RequestContext.HttpContext.Response.Cache.AppendCacheExtension("no-store, must-revalidate");
        context.RequestContext.HttpContext.Response.AppendHeader("Pragma", "no-cache");
        context.RequestContext.HttpContext.Response.AppendHeader("Expires", "0");

        base.OnActionExecuted(context);
    }
}

For global use add it to the FilterConfig.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new CustomHeaders());
    }
}

Or only use these headers on a specific controller.

[Authorize]
[CustomHeaders]
public class HomeController : Controller
{
    [AllowAnonymous]
    public ActionResult Index()

Side note: you can use IIS and web.config for other headers. For example on static content like your bundles (jquery,bootstrap). Look for these sections customheaders, staticcontent.