IIS 7 returns 304 instead of 200

According to section 14.9 of the HTTP1.1 spec, the no-cache directive for the Cache-Control header is only imposable by the origin server, which means IIS is ignoring the header in your request.

The cache-control directives can be broken down into these general categories:

  - Restrictions on what are cacheable; these may only be imposed

by the origin server.

Section 14.9.1 defines public, private, and no-cache as the directives restricting what is cacheable, which can only be imposed by the server.

If you don't want your .js file to be cached you'll either need to set the no-cache directive in the app (ie- the ASP.NET code) or you'll need to change the Cache-Control header in the request to use the no-store directive instead of no-cache.

EDIT:
Based on your comment - yes I assumed you did not want the file cached. The 304, then, might be coming as a result of the file being in one of IIS's internal caches. Have a look at these:

  • Configuring Output Caching in IIS7
  • Caching in IIS7

I have been having the same problem for a while and have all caching turned off... However, I installed the Compression module for IIS7 at some point which by default had enabled compression of static files on my existing sites. I turned all compression off for the impacted sites and now they seems to be working fine touch wood.


We were also experiencing this bug but we were using an asset management library (Cassette). After an extensive investigation of this issue, we've found that the root cause of this issue is with a combination of ASP.NET, IIS, and Cassette. I'm not sure if this is your problem (using the Headers API rather than the Cache API), but the pattern seems to be the same.

Bug # 1

Cassette sets the Vary: Accept-Encoding header as part of its response to a bundle since it can encode the content with gzip/deflate:

  • BundleRequestHandler: https://github.com/andrewdavey/cassette/blob/a2d9870eb0dc3585ef3dd542287595bc6162a21a/src/Cassette.Aspnet/BundleRequestHandler.cs#L78
  • HttpResponseUtil: https://github.com/andrewdavey/cassette/blob/a2d9870eb0dc3585ef3dd542287595bc6162a21a/src/Cassette.Aspnet/HttpResponseUtil.cs#L45

However, the ASP.NET output cache will always return the response that was cached first. For example, if the first request has Accept-Encoding: gzip and Cassette returns gzipped content, the ASP.NET output cache will cache the URL as Content-Encoding: gzip. The next request to the same URL but with a different acceptable encoding (e.g. Accept-Encoding: deflate) will return the cached response with Content-Encoding: gzip.

This bug is caused by Cassette using the HttpResponseBase.Cache API to set the output cache settings (e.g. Cache-Control: public) but using the HttpResponseBase.Headers API to set the Vary: Accept-Encoding header. The problem is that the ASP.NET OutputCacheModule is not aware of response headers; it only works via the Cache API. That is, it expects the developer to use an invisibly tightly-coupled API rather than just standard HTTP.

Bug # 2

When using IIS 7.5 (Windows Server 2008 R2), bug # 1 can cause a separate issue with the IIS kernel and user caches. For example, once a bundle is successfully cached with Content-Encoding: gzip, it's possible to see it in the IIS kernel cache with netsh http show cachestate. It shows a response with 200 status code and content encoding of "gzip". If the next request has a different acceptable encoding (e.g. Accept-Encoding: deflate) and an If-None-Match header that matches the bundle's hash, the request into IIS's kernel and user mode caches will be considered a miss. Thus, causing the request to be handled by Cassette which returns a 304:

  • BundleRequestHandler: https://github.com/andrewdavey/cassette/blob/a2d9870eb0dc3585ef3dd542287595bc6162a21a/src/Cassette.Aspnet/BundleRequestHandler.cs#L44

However, once IIS's kernel and user modes process the response, they will see that the response for the URL has changed and the cache should be updated. If the IIS kernel cache is checked with netsh http show cachestate again, the cached 200 response is replaced with a 304 response. All subsequent requests to the bundle, regardless of Accept-Encoding and If-None-Match will return a 304 response. We saw the devastating effects of this bug where all users were served a 304 for our core script because of a random request that had an unexpected Accept-Encoding and If-None-Match.

The problem seems to be that the IIS kernel and user mode caches are not able to vary based on the Accept-Encoding header. As evidence of this, by using the Cache API with the workaround below, the IIS kernel and user mode caches seem to be always skipped (only the ASP.NET output cache is used). This can be confirmed by checking that netsh http show cachestate is empty with the workaround below. ASP.NET communicates with the IIS worker directly to selectively enable or disable the IIS kernel and user mode caches per-request.

We were not able to reproduce this bug on newer versions of IIS (e.g. IIS Express 10). However, bug # 1 was still reproducible.

Our original fix for this bug was to disable IIS kernel/user mode caching only for Cassette requests like others mentioned. By doing so, we uncovered bug # 1 when deploying an extra layer of caching in front of our web servers. The reason that the query string hack worked is because the OutputCacheModule will record a cache miss if the Cache API has not been used to vary based on the QueryString and if the request has a QueryString.

Workaround

We've been planning to move away from Cassette anyways, so rather than maintaining our own fork of Cassette (or trying to get a PR merged), we opted to use an HTTP module to work around this issue.

public class FixCassetteContentEncodingOutputCacheBugModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PostRequestHandlerExecute += Context_PostRequestHandlerExecute;
    }

    private void Context_PostRequestHandlerExecute(object sender, EventArgs e)
    {
        var httpContext = HttpContext.Current;

        if (httpContext == null)
        {
            return;
        }

        var request = httpContext.Request;
        var response = httpContext.Response;

        if (request.HttpMethod != "GET")
        {
            return;
        }

        var path = request.Path;

        if (!path.StartsWith("/cassette.axd", StringComparison.InvariantCultureIgnoreCase))
        {
            return;
        }

        if (response.Headers["Vary"] == "Accept-Encoding")
        {
            httpContext.Response.Cache.VaryByHeaders.SetHeaders(new[] { "Accept-Encoding" });
        }
    }

    public void Dispose()
    {

    }
}

I hope this helps someone 😄!