32

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.

Community
  • 1
  • 1
JK.
  • 20,010
  • 29
  • 124
  • 204
  • 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 Mar 16 '14 at 23:00
  • 1
    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. Mar 17 '14 at 00:41

3 Answers3

54

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:

Community
  • 1
  • 1
JK.
  • 20,010
  • 29
  • 124
  • 204
  • 5
    I love how MVC and IIS just decide to randomly break with their own API here and there and introduce silent failures. – A.R. Sep 22 '15 at 13:55
  • Works on IIS 8 too, as does this answer: http://stackoverflow.com/a/33065747/318411 – Whelkaholism Sep 23 '16 at 10:40
  • 1
    This is the only thing that worked for me, even on IIS 10 on Windows 10. Thank you! Other answers that set the "no cache" header did not cause the "no-store" value to be outputted, which was causing the browser to cache some content and load it when the user navigated with the back / forward browser buttons. – mfranchi Feb 19 '18 at 16:56
  • I forced IIS to make response header to be "private, no-store, max-age=0" through Global.asax.cs and controller action filter, but IIS showed it like "public, no-store, max-age=0". Finally this method works for me in IIS 10 as well. Great Helps! – quasar Mar 08 '20 at 07:18
1

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,
        };
    }
}
Veysel Ozdemir
  • 625
  • 7
  • 12
  • The ActionFilterAttribute OnActionRequested seems to need an ActionExecutedContext not an HttpActionExecutedContext . The ActionExecutedContext doesnt' allow access the ActionContex (only the HttpContext.Response.Headers, which doesn't have CacheControl) any ideas? (note MVC not WebAPI) – Andiih Sep 20 '19 at 13:37
1

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.

ivw
  • 170
  • 2
  • 6