OutputCache is sending wrong Vary header when the call hits the cache
Asked Answered
C

3

11

I have an action method that I want to cache:

[OutputCache(Duration=60*5, Location=OutputCacheLocation.Any, VaryByCustom="index")]
public ActionResult Index()
{
    return View();
}

With this approach:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    context.Response.Cache.SetOmitVaryStar(true);
    context.Response.Cache.VaryByHeaders["Cookie"] = true;

    if (User.Identity.IsAuthenticated)
    {
        Debug.Print("Authenticated");
        context.Response.Cache.SetNoServerCaching();
        context.Response.Cache.SetCacheability(HttpCacheability.Private);
        return null;
    }
    else
    {
        Debug.Print("Non authenticated");
        return custom;
    }
}

The idea was to keep a cached version of the page for non-authenticated users, but avoid caching for authenticated ones.

I thought it will always return a Vary:Cookie HTTP header, but it is not. Doing a test with Fiddler and issuing twice the same request, in the first HTTP call it goes good:

HTTP/1.1 200 OK
Cache-Control: public, max-age=300
Content-Type: text/html; charset=utf-8
Expires: Thu, 09 Feb 2012 10:53:36 GMT
Last-Modified: Thu, 09 Feb 2012 10:48:36 GMT
Vary: Cookie
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 09 Feb 2012 10:48:37 GMT
Content-Length: 441

But in the second one, it overwrites the header:

HTTP/1.1 200 OK
Cache-Control: public, max-age=297
Content-Type: text/html; charset=utf-8
Expires: Thu, 09 Feb 2012 10:53:36 GMT
Last-Modified: Thu, 09 Feb 2012 10:48:36 GMT
Vary: *
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 09 Feb 2012 10:48:39 GMT
Content-Length: 441

So, as far as I know, browsers won't cache the request even if it is public, since Vary:* means that the request has been generated with parameters that are not in the URL nor in the HTTP headers. Is there a way to fix this?

Regards.

UPDATE:

In a similar way, when I send two identical authenticated requests, the first call gets the private modifier, but not the Vary header:

HTTP/1.1 200 OK
Cache-Control: private, max-age=300
Content-Type: text/html; charset=utf-8
Expires: Thu, 09 Feb 2012 12:43:14 GMT
Last-Modified: Thu, 09 Feb 2012 12:38:14 GMT
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 09 Feb 2012 12:38:14 GMT
Content-Length: 443

But the second one gets the same response that a non-authenticated request:

HTTP/1.1 200 OK
Cache-Control: public, max-age=298
Content-Type: text/html; charset=utf-8
Expires: Thu, 09 Feb 2012 12:44:32 GMT
Last-Modified: Thu, 09 Feb 2012 12:39:32 GMT
Vary: *
Server: Microsoft-IIS/7.5
X-AspNetMvc-Version: 3.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Thu, 09 Feb 2012 12:39:33 GMT
Content-Length: 443

I have uploaded a test project showing the issue so may be you want to give it a try.

Please be aware that there is an IHttpModule that sets a request as authenticated or not depending on if the request has a cookie or not, this is not a "real life" approach, it is just for testing purposes.

The project contains only a web page with a link to itself, a link that logs you in, and another link that logs you out:

  • LogIn : Sends a cookie in a HTTP 302 redirection to the home page again.
  • LogOut: Sends a expired cookie in a HTTP 302 recirection to the home page again.

The expected/ideal behaviour would be:

  1. User access Index, and get the page from the server. The page show date "A".
  2. User access Index again, and the browser shows the cached version.The page show date "A".
  3. Clean browser cache.
  4. User access Index again, and the browser shows the server cached version. The page show date "A".
  5. User clicks login, and the broswer gets a new page, that show date "B".
  6. User clicks logout, and the browser gets the server cached page. The page show date "A" again.

But this is the behaviour so far:

  1. User access Index, and get the page from the server. The page show date "A".
  2. User access Index again, and the browser shows the cached version.The page show date "A".
  3. Clean browser cache.
  4. User access Index again, and the browser shows the server cached version. The page show date "A".
  5. User clicks login, and the broswer gets a new page, that show date "B".
  6. User clicks logout, and the browser should get the server cached page, but it does not. The page show date "B" again from the browser cache. This is because the lack of the Vary header in the authenticated response.

I don't know if I get something wrong about caching, just missing some detail or the OutputCache does not work very well, but I would appreciate any guidance.

Cheers.

UPDATE 2:

My intention is to use the HTTP cache semantics to:

  1. Allow browsers and proxys to cache the "public" version of the page.
  2. Allow browsers to cache the "authenticated" version of the page for its user.

If I change the OutputCache declaration to do the caching only on the server and prevent the downstream and client caching:

[OutputCache(Duration=60*5, Location=OutputCacheLocation.Server, VaryByCustom="index")]

it behaves as expected, but the downstream and client cache is prevented, and that is not what I want.

Chattanooga answered 9/2, 2012 at 12:10 Comment(6)
What happens if you also set the other VaryBy properties on the method's OutputCache attribute?Premise
I have added VaryByHeader="Cookie" , and it still happens, the second call gets a Vary=*.Chattanooga
What if you try the approach linked to in the linked question as-is, without varying-by-cookie or modifying the response headers inside the GetVaryByCustom method? (Technically, you don't need to vary-by-cookie to achieve caching only for anonymous users.)Premise
Oh yes, that works but it is not using the HTTP cache semantics. If I set the OutputCache declaration with "Location=OutputCacheLocation.Server", then it works perfectly. The point of this post, is to take advantage of HTTP caching.Chattanooga
Your question contains contradictory information: the title mentions OutputCache, and you say you want to "keep a cached version of the page for non-authenticated users, but avoid caching for authenticated ones.", but then go on to talk about HTTP cache semantics.Plage
It is not contradictory. On the server(Output caching), I want to keep a cached version of the public page only. On the clients (HTTP cache semantics), I want to keep a cached version of the public page, or the authenticated page (Vary:Cookie). On the downstream(HTTP Cache semantics), I want to cache the public page, but not the authenticated ones. Where is the contradiction?Chattanooga
L
3

I don't think the [OutputCache] attribute is what you want, the VaryByCustom method is basically saying that I want to cache different versions based on these parameters, it doesn't really have an option for Do Not Cache and the majority of the code in the attribute is built around server based caching.

That being said the documentation on MSDN for custom caching seems to indicate you need to return a string to vary on based on the authentication state:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    if(custom == "user") return "User:" + context.Request.User.Identity.Name;

    return base.GetVaryByCustomString(context, custom);
}

And then use the user literal in the VaryByCustom:

[OutputCache(Duration=60*5, Location=OutputCacheLocation.Any, VaryByCustom="user")]
public ActionResult Index()
{
    return View();
}

So basically this would result in a cache being built for anonymous (assuming the anonymous identity is empty string or something) and every user on the server, and a Vary: * sent to the client I believe. Obviously not ideal what you are looking for.

If you really just want to cache the unauthenticated version using HTTP caching I would recommend not using the OutputCacheAttribute and using something more custom.

You could easily just write in your own custom attribute something like what you have for your GetVaryByCustomString implementation (this is just some pseudo code, would need more than this):

public class HttpCacheUnauthenticatedAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if(!filterContext.HttpContext.Request.IsAuthenticated) {
            //TODO: set unauthenticated caching values and vary here
        }
    }
}

And then tag your action method with it:

[HttpCacheUnauthenticated]
public ActionResult Index()
{
    return View();
}
Lug answered 17/2, 2012 at 16:41 Comment(5)
The approach with OutputCache is working as expected in the first call, it is just that is returning the wrong Vary header in subsequent calls, when the calls are a "cache hit" in the OutputCache. That is all. The functionality is provided, but not working as expected. Implement my own OutputCache action filter won't solve anything. The problem is not the OutputCacheAttribute itself, but the out put caching API behind.Chattanooga
@Chattanooga Thats why I think you should bypass the API and write headers yourself. You are only looking for HTTP caching, not server side caching correct? Like I said the OutputCache attribute is not designed for any scenario in which its content is not cached on a response.Lug
No, I am looking for both :D Read carefully the "The expected/ideal behaviour would be:" section. Cheers.Chattanooga
Even with server caching, this isn't what OutputCache is designed for, it vary's caching, it doesn't have a DisableCachingIf option...I think if you want to continue to use that attribute, you need to give up the don't cache option and just use the appropriate vary values for your needs.Lug
Even if I do it as you say, the second call gets a wrong HTTP Vary header. That is what I am complaining about :)Chattanooga
W
1

Sort of wrestling with something similar myself. Have you tried in the web.config to the setting omitVaryStar=true

https://msdn.microsoft.com/en-us/library/ms228124(v=vs.100).aspx

Wold answered 13/4, 2012 at 5:39 Comment(0)
B
0

I am using a custom cache provider and in this case there is a simple solution for this. On the BeginRequest, based on the user authentication status, we set a context information to not run cache:

HttpContext.Current.Items["NoCache"] = "1";

And then on our GetVaryBy method we return null if this information is set:

public override string GetVaryByCustomString(HttpContext context, string custom)
{
    if (HttpContext.Current.Items["NoCache"] != null)
        return null;

    // remaining code here
}

And then on the cache methods, we can test the same. For instance:

public override object Add(string key, object entry, DateTime utcExpiry)
{
    if (HttpContext.Current.Items["NoCache"] != null)
        return null;

    // remaining code here
}
Byyourleave answered 27/11, 2012 at 18:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.