Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Set-Cookie Headers getting stripped in ASP.NET HttpHandlers


:P
On this page:

Yikes, I ran into a real bummer of an edge case yesterday in one of my older low level handler implementations (for West Wind Web Connection in this case). Basically this handler is a connector for a backend Web framework that creates self contained HTTP output. An ASP.NET Handler captures the full output, and then shoves the result down the ASP.NET Response object pipeline writing out the content into the Response.OutputStream and seperately sending the HttpHeaders in the Response.Headers collection.The headers turned out to be the problem and specifically Http Cookies, which for some reason ended up getting stripped out in some scenarios.

My handler works like this: Basically the HTTP response from the backend app would return a full set of HTTP headers plus the content. The ASP.NET handler would read the headers one at a time and then dump them out via Response.AppendHeader().

But I found that in some situations Set-Cookie headers sent along were simply stripped inside of the Http Handler. After a bunch of back and forth with some folks from Microsoft (thanks Damien and Levi!) I managed to pin this down to a very narrow edge scenario.

It's easiest to demonstrate the problem with a simple example HttpHandler implementation. The following simulates the very much simplified output generation process that fails in my handler. Specifically I have a couple of headers including a Set-Cookie header and some output that gets written into the Response object.

using System.Web;

namespace wwThreads
{

    public class Handler : IHttpHandler
    {
        /* NOTE: 
         * 
         * Run as a web.config set handler (see entry below)
         *
         * Best way is to look at the HTTP Headers in Fiddler
         * or Chrome/FireBug/IE tools and look for the 
         * WWHTREADSID cookie in the outgoing Response headers
         * ( If the cookie is not there you see the problem! )
         */
        public void ProcessRequest(HttpContext context)
        {                        
            HttpRequest request = context.Request;
            HttpResponse response = context.Response;

            // If ClearHeaders is used Set-Cookie header gets removed!
            // if commented header is sent...
            response.ClearHeaders();

            response.ClearContent(); 
                                
            // Demonstrate that other headers make it
            response.AppendHeader("RequestId", "asdasdasd");
            
            // This cookie gets removed when ClearHeaders above is called
            // When ClearHEaders is omitted above the cookie renders
            response.AppendHeader("Set-Cookie", "WWTHREADSID=ThisIsThEValue; path=/");

           // *** This always works, even when explicit 
           //     Set-Cookie above fails and ClearHeaders is called
           //response.Cookies.Add(new HttpCookie("WWTHREADSID", "ThisIsTheValue"));

            response.Write(@"Output was created.<hr/>
                           Check output…");            
        }

        public bool IsReusable
        {
            get { return false; }
        }

    }
}

In order to see the problem behavior this code has to be inside of an HttpHandler, and specifically in a handler defined in web.config with:

      <add name=".ck_handler"
           path="handler.ck"
           verb="*"
           type="wwThreads.Handler"
           preCondition="integratedMode" />

Note: Oddly enough this problem manifests only when configured through web.config, not in an ASHX handler, nor if you paste that same code into an ASPX or .csHtml page.

It does however also manifest in an MVC ActionMethod:

public class CookieBugController : Controller
{
    //
    // GET: /CookieBug/

    public string Index()
    {
        var response = HttpContext.Response;

        // If ClearHeaders is used Set-Cookie header gets removed!
        // if commented header is sent...
        response.ClearHeaders();

        //RemoveHeaders(response);

        response.ClearContent();

        // Demonstrate that other headers make it - this one shows
        response.AppendHeader("RequestId", "0ae133f123d");

        // This cookie gets removed when ClearHeaders above is called
        // When ClearHEaders is omitted above the cookie renders
        response.AppendHeader("Set-Cookie", "WWTHREADSID=ThisIsThEValue; path=/");

        // *** This always works, even when explicit 
        //     Set-Cookie above fails and ClearHeaders is called
        //response.Cookies.Add(new HttpCookie("WWTHREADSIsD", "ThisIsTheValue"));            

        return @"Output was created.<hr/>Check output with Fiddler or HTTP Proxy to see whether cookie was sent.";
    }
}

It appears if the request is not bound to a physical file on disk (ASHX, ASPX etc. pages), then this fails. Bind to a file and it works. For example, if I create a Handler.ck file for the handler the problem goes away.

What's the problem exactly?

The code above simulates the more complex code in my live handler that picks up the HTTP response from the backend application and then peels out the headers and sends them one at a time via Response.AppendHeader. One of the headers in my app can be one or more Set-Cookie.

I found that the Set-Cookie headers were not making it into the Response headers output. Here's the Chrome Http Inspector trace:

NoCookies

Notice, no Set-Cookie header in the Response headers!

Now, running the very same request after removing the call to Response.ClearHeaders() command, the cookie header shows up just fine:

CookieThere

As you might expect it took a while to track this down. At first I thought my backend was not sending the headers but after closer checks I found that indeed the headers were set in the backend HTTP response, and they were indeed getting set via Response.AppendHeader() in the handler code. Yet, no cookie in the output.

In the simulated example the problem is this line:

response.AppendHeader("Set-Cookie", "WWTHREADSID=ThisIsThEValue; path=/");

which in my live code is more dynamic ( ie. AppendHeader(token[0],token[1[]) )as it parses through the headers.

Bizzaro Land: Response.ClearHeaders() causes Cookie to get stripped

Now, here is where it really gets bizarre: The problem occurs only if:

  • Response.ClearHeaders() was called before headers are added
  • Response.AppendHeader("Set-Cookie","…") was called
  • If there's no physical file: web.config handler, or MVC Routed Controller Action
    Never a problem in ASHX, ASPX, csHtml files etc.
  • It only occurs if there are WebPages files (.cshtml,.vbhtml) present in the project tree
  • Occurs only in IIS and IIS Express but not in the Visual Studio Web Server

So in the code above if you remove the call to ClearHeaders(), the cookie gets set!  Add it back in and the cookie is not there.

If I run the above code in an ASHX handler it works. If I run the same code inside of ASPX page it works. Only in the HttpHandler configured through Web.config does it fail! If I use it inside of an MVC Controller I also get the failure, but it's not very likely that one would use Set-Cookie headers in a controller method I suspect :-) If I remove any .cshtml files from my web application it also works, but of course for MVC applications that's not really an option.

Cue the Twilight Zone Music.

This problem is the weirdest edge case I've ever run into and it took forever to pin it down exactly. In fact on several occasions I posted some code on CodePaste.net only to find out that nobody could reproduce it. No wonder with all the odd dependencies - the final straw was the requirement of ASP.NET WebPages being present. I suspect that this is the real culprit - some sort of pre or post processing that mucks with the headers.

Workarounds

As is often the case the fix for this once you know the problem is not too difficult. The difficulty lies in tracking inconsistencies like this down. Luckily there are a few simple workarounds for the Cookie issue.

Don't use AppendHeader for Cookies

The easiest and obvious solution to this problem is simply not use Response.AppendHeader() to set Cookies. Duh! Under normal circumstances in application level code there's rarely a reason to write out a cookie like this:

response.AppendHeader("Set-Cookie", "WWTHREADSID=ThisIsThEValue; path=/");

but rather create the cookie using the Response.Cookies collection:

response.Cookies.Add(new HttpCookie("WWTHREADSID", "ThisIsTheValue"));

Unfortunately, in my case where I dynamically read headers from the original output and then dynamically  write header key value pairs back  programmatically into the Response.Headers collection, I actually don't look at each header specifically so in my case the cookie is just another header.

My first thought was to simply trap for the Set-Cookie header and then parse out the cookie and create a Cookie object instead. But given that cookies can have a lot of different options this is not exactly trivial, plus I don't really want to fuck around with cookie values which can be notoriously brittle.

Don't use Response.ClearHeaders()

The real mystery in all this is why calling Response.ClearHeaders() prevents a cookie value later written with Response.AppendHeader() to fail. I fired up Reflector and took a quick look at System.Web and HttpResponse.ClearHeaders. There's all sorts of resetting going on but nothing that seems to indicate that headers should be removed later on in the request. The code in ClearHeaders() does access the HttpWorkerRequest, which is the low level interface directly into IIS, and so I suspect it's actually IIS that's stripping the headers and not ASP.NET, but it's hard to know. Somebody from Microsoft and the IIS team would have to comment on that.

In my application it's probably safe to simply skip ClearHeaders() in my handler. The ClearHeaders/ClearContent was mainly for safety but after reviewing my code there really should never be a reason that headers would be set prior to this method firing.

However, if for whatever reason headers do need to be cleared, it's easy enough to manually clear the headers out:

private void RemoveHeaders(HttpResponse response)
{
    List<string> headers = new List<string>();
    foreach (string header in response.Headers)
    {
        headers.Add(header);
    }
    foreach (string header in headers)
    {
        response.Headers.Remove(header);
    }

    response.Cookies.Clear();
}

Now I can replace the call the Response.ClearHeaders() and I don't get the funky side-effects from Response.ClearHeaders().

Summary

I realize this is a total edge case as this affects only the uncommon use of explicitly appending a Cookie Header via Response.AppendHeader. And even then it occurs only if you are running in a project that also contains ASP.NET WebPages files and you're executing a non-file bound request. Lots of odds against that happening. Nevertheless there's a bug here that can bite you, as it did me.

For most application level code this will likely never be a problem. It's so much easier using Response.Cookies.Add() that AppendHeader() for cookie usage should never occur, except in some sort of system component such as mine that directly parses headers into the Http output stream.

IAC, there are workarounds to this should you run into it, although I bet when you do run into it, it'll likely take a bit of time to find the problem or even this post in a search because it's not easily to correlate the problem to the solution. It's quite possible that more than cookies are affected by this behavior. Searching for a solution I read a few other accounts where headers like Referer were mysteriously disappearing, and it's possible that something similar is happening in those cases.

Again, extreme edge case, but I'm writing this up here as documentation for myself and possibly some others that might have run into this.

Epilog
The bug was confirmed by Levi Broderik (from Microsoft) and filed for a runtime fix. As I guessed it's a problem in the ASP.NET WebPages Engine routing code apparently with some overly aggressive Cookie clearing logic. It occurs only when no file is present because WebPages Routing supports extensionless URLs for any .cshtml/.vbhtml page (ie. /HelloWorld.csHtml is same as /HelloWorld).

Resources

Posted in ASP.NET  IIS7  

The Voices of Reason


 

Doug Dodge
November 30, 2012

# re: Set-Cookie Headers getting stripped in ASP.NET HttpHandlers

Hey, someone's got to take the arrows. :)

BTW, as a part of my learning .NET, C# and such I was investigating data-driven scenarios as it relates to localization. Man, you own it. Everyone was using you as their primary source.

Andrei Rinea
December 01, 2012

# re: Set-Cookie Headers getting stripped in ASP.NET HttpHandlers

Strange one, Rick.

Two small questions :

1. Do you use Reflector these days? There are so many nice free alternatives (JustDecompile, IL Spy etc.)

2. Have you tried submitting this (apparent) bug to Microsoft Connect?

Thanks.

Rick Strahl
December 02, 2012

# re: Set-Cookie Headers getting stripped in ASP.NET HttpHandlers

@Andrei - Yup still use Reflector. Yes there are alternatives today, but Reflector IMHO still has the best UI to do this.

As to reporting as a bug - I've not been able to repro this reliably unfortunately. It's an odd edge case. It happens consistently in my live app, but when I copied just the test handler into a new project it no longer failed. I checked everything I can think of - handlers, modules defined in the new site but I can't find the difference. Fails in one project not the other <shrug>

It'll remain a mystery I guess.

carlshen
June 02, 2013

# re: Set-Cookie Headers getting stripped in ASP.NET HttpHandlers

You master:
Customer's client is to use TR069,use the web service using the SOAP protocol to pass the value.when the client side when you first registered.server-side (web service) after receiving information, to add HTTP Header Set-Cookie: JSESSIONID, back to the client side,client-side at the next ceremonial vessel Pops, the same cookie value to return pass, but when the server Response Header not on the Set-Cookie: JSESSIONID
SOAP Content is no problem, only a thin Set-Cookie: JSESSIONID = F0A799587E011238E1311CD11B24A969; Path = / acs not on the increase
Roughly the code below:
Dim request As HttpRequest = Context.Request
    Dim response As HttpResponse = Context.Response
    response.Clear()
    response.ClearContent()
    response.ClearHeaders()
    response.ContentType = "text/xml"
    response.Charset = "UTF-8"
    response.AddHeader("Set-Cookie", "JSESSIONID=4567845226")

Respones Header
Date: Sun, 02 Jun 2013 06:44:22 GMT
Content-Encoding: gzip
Server: Microsoft-IIS/7.5
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
Vary: Accept-Encoding
Content-Type: text/xml;
charset=utf-8
Cache-Control: private
Content-Length: 395
Set-Cookie: JSESSIONID = F0A799587E011238E1311CD11B24A969; Path = /

Remoraz
December 31, 2014

# re: Set-Cookie Headers getting stripped in ASP.NET HttpHandlers

Hey Rick, late to the party, I know.

Excellent write up. Very helpful stuff, even a version later. I appreciate your summary, especially when you said, "just don't clear the headers, but if you HAD to, do it like this...".

One minor thing: you have a Cookies.Clear() in that clean up method. That won't work, I'm afraid - had issues with it myself. You need to loop the cookies and set the expiry to "yesterday" (DateTime.Now minus a day). Just throwing that in to, MAYBE, help someone avoid a bug in the future.

Great article! Thanks for the contribution!

Celso
December 07, 2021

# re: Set-Cookie Headers getting stripped in ASP.NET HttpHandlers

Rick,

Almost 2022 and I'm still experiencing the same issue here. But thanks to you I was able to pinpoint the source of it. One has to remove the WebDAV modules and handlers in order to get rid of this bug. Hope it helps as much as your post has helped me.


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024