0

Experiencing really weird behavior on a site I maintain, https://conservationx.com/

1) Global.asax Session_Start, simply checks for a cookie of a specific name (visitor) and if not present, we issue one.

protected void Session_Start()
{
    var req = HttpContext.Current.Request;
    var cookie = request.Cookies["visitor"];
    if (cookie == null)
    {
        var cookie = new HttpCookie("visitor", sguid);
        cookie.Shareable = false;   // Do not store in outputcache
        cookie.Expires = DateTime.UtcNow.AddYears(1);
        response.SetCookie(cookie);

2) In local Debug, I can visit and step into it just fine, first request - Incognito window consistently gets me Set-Cookie header on first request.

3) Once deployed, I will never, ever see that Cookie issued on first request. I consistently see it on second request.

4) In addition to seeing Set-Cookie header not appear until the second request in DevTools Network tab, I've also added logging to the Session_Start body, and wrapped it all in try/catch, logging any exceptions. That likewise consistently fails to fire on first visit. This particular site checks if you're logged in once the page is done loading, with a call to /account/navaccountinfo - fresh browsers/Incognito windows will always fail to get a cookie set landing on any page on the site, then see the cookie finally set on that secondary load, and sure enough our logs are filled with cookies being set on the request to that secondary request URL.

Is this a known issue with deploying ASP.Net MVC to IIS 7.5? We are using OWIN with Identity Framework if that has any impact?

Speculating, I wonder if this is related:

OutputCache VaryByCustom cookie value

I set cookie.Shareable = false because the visitor Id is meant to be unique per browser. Can't be giving it out to multiple people via the Server's OutputCache. The initial page visited, like / or /about, has an OutputCacheAttribute set and is visited via GET. The followup, /account/navaccountinfo, is visited via POST, and so, obviously never cached. So I wonder if this is actually a bad interaction between OutputCache and cookie.Shareable = false.

Chris Moschini
  • 36,764
  • 19
  • 160
  • 190
  • Then did you check how the first and second requests look like? – Lex Li Jun 19 '18 at 16:47
  • This does appear to be a caching issue. Essentially, I cache very little or not at all in my Dev env, but, obviously cache more aggressively on the server-side. By waiting for Session to start, I was waiting until too late - after the Cache fired. That said, I've worked around this but - probably poorly. I'll post my bad workaround in a bit, but the deeper I dig the more I find people struggling with this issue, and the above Answer that flat-out hacks the OutputCache feels... overaggressive? – Chris Moschini Jun 19 '18 at 16:55
  • Remember to post as an answer and accept it. – Lex Li Jun 19 '18 at 17:24
  • Well, it's not a GOOD answer, so it's there for info but I want to encourage a better one. – Chris Moschini Jun 19 '18 at 19:58

1 Answers1

0

This is not a GOOD answer, but this is an answer and I'm eagerly interested in other more elegant solutions.

The Problem

We don't cache much or at all in Dev, we do on the Server, thus the Session not firing until a Post fired - the user was seeing a quick cached response from IIS, which means most of the Asp.Net pipeline never spins up, especially Session_Start. Posts always bust through the cache and voila, second call - which was always a Post - always saw the cookie set, and always saw a log entry for the Session.

In addition, the fact that setting HttpCookie.Shareable = false essentially throws a firebomb into your caching by default is very poorly documented, and by not using much caching in Dev, we weren't really seeing the major damage this flag does.

The Bad Solution (Elegant Proposals Welcome)

We're already making heavy use of OutputCache VaryByCustom, where we do some somewhat creative things like serving the general public one version of the site, and logged-in members each their own. The details of how this works are beyond the scope of this issue, but suffice to say we use VaryByCustom in Global.asax:

public override string GetVaryByCustomString(HttpContext context, string key)
{
    return VaryByCustomKeyHandler.HandleCacheKey(key, context, Application);
}

The key is just our compile-safe way of indicating what kind of variance we want for a Controller Action, and I'll skip the details of that, but, the HandleCacheKey method now looks like this:

string uidInSetVisitorCookie = context.Request.ServerVariables["SetVisitorCookie"];
if (uidInSetVisitorCookie != null)
    return uidInSetVisitorCookie;   // Force vary

So, if the user is supposed to receive a Cookie, we basically tell the OutputCache that this user and this user alone should get their very own copy of this page, for now. We do that by providing a cache key that happens to be their randomly generated Uid. That Uid is generated in BeginRequest, which, I've verified, does fire even when OutputCache is ultimately going to handle the Response:

protected void Application_BeginRequest(object sender, EventArgs ev)
{
    var req = (Brass9.Web.Visiting.Req)HttpContext.Current.Request;
    string uid = AnonCookieManager.O.GetUid(req);
    bool hadNoCookie = String.IsNullOrEmpty(uid);
    if (hadNoCookie)
    {
        uid = (Brass9.ShortGuid.NewGuid()).ToString();
        AnonCookieManager.O.PutCookie(HttpContext.Current.Response, uid);
        req.ServerVariables["SetVisitorCookie"] = uid;
    }

So there's a number of light library calls in there, but quickly:

  • Req is a simple wrapper class for the 2 annoying Request classes Asp.Net uses that have a ton of overlap yet fail to share a base class.
  • GetUid() gets the visitor cookie out of the Request cookies, if any.
  • ShortGuid is a lot like this

Most importantly, PutCookie() has been modified to participate in this workaround, like so:

var cookie = new HttpCookie(CookieName, sguid);

//cookie.Shareable = false;
// Cookies shouldn't be shared, but, we signal to vary the OutputCache instead
// of blowing up OutputCache

cookie.Expires = DateTime.UtcNow.AddYears(1);
response.SetCookie(cookie);
Chris Moschini
  • 36,764
  • 19
  • 160
  • 190