Search code examples
asp.netiishttpresponsehttphandler

Response from httpHandler is randomly missing characters on client


Problem:

A response from our .NET site is often randomly missing parts of the original (in two-character chunks) when received by the browser

Details:

We have a custom httpHandler called CssCombineHandler that combines the content of multiple CSS files into one axd file.

It is specified in the web.config like this:

<httpHandlers>
  <add path="css.axd" verb="*" type="MyNamespace.CssCombineHandler, MyNamespace"/>
</httpHandlers>

It's called via a URL like this...

http://example.com/css.axd?files=..%2fcss%2fFile1.css%2c..%2fcss%2fFile2.css%2c..%2fcss%2fFile3.css

And all the files listed in the files param get combined into the response.

Relevant code is something like this:

    context.Response.ClearHeaders()
    context.Response.ContentType = "text/css"        
    context.Response.Cache.SetExpires(someDate)
    context.Response.Cache.SetMaxAge(someOtherDate)
    context.Response.Cache.SetCacheability(HttpCacheability.Public)
    context.Response.Cache.VaryByParams("files") = True    
    context.Response.BufferOutput = False  'See notes about this below

    Dim responseBody As New StringBuilder()

    '(Loop through files here)
    responseBody.Append(File.ReadAllText(context.Server.MapPath(currentFileUri.AbsolutePath)))
    '(end loop)

    context.Response.Write(responseBody.ToString())  'Here it has the full, correct string!

But when you inspect the response (e.g. in Firefox or Chrome dev tools Network tab), it usually has at least one place where two characters in a row have been removed. The effect is random based on where it happens, but for example, this CSS in the original...

visibility:visible!important;

...might get changed to...

visibilitvisible!important;

I've seen this happen 0-3 times in the combined file, sort of depending on its total size. Any change to the content of the combined file (i.e. changes to one of the files being combined, or changes in the list of files being combined) can result in the missing characters moving to a different place.

Things I've tried:

I haven't worked with stuff like this much, so forgive my ignorance. This is what I've tried so far.

  • Setting Response.BufferOutput = True. This appears to fix it at first. I can see the response now has a content-length header that was generated automatically and no longer has a Transfer-Encoding: chunked header.

This works on small responses but if I have more than a couple files being combined it's like the response is too big and something (.NET? IIS?) is forcing it back to chunked mode, which then has missing characters again. I don't know the threshold, but for example a 15k response will buffer correctly but an 82k response will not

  • I thought maybe it was an encoding issue, since some css files are UTF-8 and some are Windows-1252. I changed all of them to UTF-8 manually, added
 context.Response.ContentEncoding = Encoding.UTF8
 context.Response.Charset = "UTF-8"

and changed to

File.ReadAllText(context.Server.MapPath(currentFileUri.AbsolutePath), Encoding.UTF8)

...but it didn't make a difference. Which actually make sense because I think the string in the Response.Write() was already correct, so it doesn't seem like a problem combining the different files.

  • Confirmed it's not an issue with gzip by disabling static and dynamic compression in IIS

  • Tried setting the ASP Response Buffering Limit to 2147483647 in IIS

  • Tried manually setting a Content-Length header to see if it might override the automatic chunking (per some things I read like this). I don't know how to get the proper size, but even testing like this, no Content-Length header would show up on the response at all.

context.Response.AddHeader("Content-Length", "500000")
  • Tried using an OutputStream instead of Response.Write():
Dim bytes As Byte() = System.Text.Encoding.UTF8.GetBytes(responseBody.ToString())
context.Response.OutputStream.Write(bytes, 0, bytes.Length)

Notes:

  • ASP.NET 4.0, Web Forms application
  • IIS 10, with app pool running in Classic mode
  • This handler does not call Response.Flush()
  • This handler does not touch Response.Filter
  • While I describe the issue as random, it's still consistent enough to not be a weird networking glitch or something. I can reproduce it locally, as well as on our beta and prod servers. For the same combined file at a moment in time, I can consistently reproduce a given response on the client.

Solution

  • It turns out we had a separate http module where we were setting a Response.Filter to dynamically replace URLs in the CSS files with their correct values for the environment (e.g. beta vs production URLs).

    Apparently when you use a filter it uses an internal stream or something, and I'm still not sure exactly why but something was going wrong with combining the various streams (when the total content was too big for one stream?) into the final output on the response.

    We fixed it by avoiding running this filter on the combined css.axd file and only doing it on normal css files (in case any are hit directly). Then in our cssCombineHandler we run the same replace logic on our combined CSS string right before the response.write().