Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC4 Script Bundles Caching issue

We have an MVS application where we are bundling the javascript code using Bundle class ( don't do minification).

Bundling just works fine, but when we run the application, Cache value is set to Cache-Control:no-cache and at the same time every time we refresh the page the request always has a 200 OK. This means the js is not getting cached on client even though nothing was changed.

Also is there a way to verify if the bundled js is built dynamically or getting it from server cache?

Thanks

like image 411
Bhargava Avatar asked Apr 14 '14 05:04

Bhargava


People also ask

How do you override the bundling setting of Web config?

To enable bundling and minification, set the debug value to "false". You can override the Web. config setting with the EnableOptimizations property on the BundleTable class. The following code enables bundling and minification and overrides any setting in the Web.

What is difference between bundling and minification?

Both bundling and minification are the two separate techniques to reduce the load time. The bundling reduces the number of requests to the Server, while the minification reduces the size of the requested assets.

How bundling works in MVC?

Creating a Bundle in MVC Bundling can create separate requests for CSS and JavaScript files. For example, if an application uses both the bootstrap and site CSS for UI design, we can create a common bundle for them. After Bundling, we need to register this bundle in the Application.


3 Answers

I was seeing the same behavior as described in codeplex link that mentions the issue:

i.e. if I visit these URLs in the following order then the behaviour is -

bundle.css?v=1234 : no-cache
bundle.css : public
bundle.css?v=1234 : public

I decided to dig into the System.Web.Optimization source code a bit to see what was going on. On the Bundle class, there is a private method setting headers, and it appears to be falling into this code:

if (noCache) {
    cachePolicy.SetCacheability(HttpCacheability.NoCache);
}

The noCache variable is set via a parameter. The calling method in this case is setting it:

// Set to no-cache if the version requested does not match
bool noCache = false;
var request = context.HttpContext.Request;
if (request != null) {
    string queryVersion = request.QueryString.Get(VersionQueryString);
        if (queryVersion != null && bundleResponse.GetContentHashCode() != queryVersion) {
                noCache = true;
    }
}

Long story short, we had switched to using Azure CDN for our bundles and changed the version query string parameter to something like ?v=1.0.0.0 based on the assembly version (similar to on this question). The bundle code is comparing "1.0.0.0" with the SHA256 hash code of the bundle content and flagging the bundle for no-cache as a result.

I resolved this by updating our query string to match the content hash.

Unfortunately the access level for the GetContentHashCode method is marked internal, so it was necessary to replicate the implementation. I ended up creating a class that inherited from Bundle so that it could apply the version number to the CdnPath as a transform:

public class ProxiedCdnBundle : Bundle
{
    private readonly string _cdnHost;

    public ProxiedCdnBundle(string virtualPath, string cdnHost = "")
        : base(virtualPath)
    {
        _cdnHost = cdnHost;
    }

    public override BundleResponse ApplyTransforms(BundleContext context, string bundleContent, IEnumerable<BundleFile> bundleFiles)
    {
        var response = base.ApplyTransforms(context, bundleContent, bundleFiles);

        if (context.BundleCollection.UseCdn && !String.IsNullOrWhiteSpace(_cdnHost))
        {
            string path = System.Web.VirtualPathUtility.ToAbsolute(context.BundleVirtualPath);
            base.CdnPath = string.Format("{0}{1}?v={2}", _cdnHost, path, GetBundleHash(response));
        }

        return response;
    }


    private static string GetBundleHash(BundleResponse response)
    {
        using (var hashAlgorithm = CreateHashAlgorithm())
        {
            return HttpServerUtility.UrlTokenEncode(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(response.Content)));
        }
    }

    private static SHA256 CreateHashAlgorithm()
    {
        if (CryptoConfig.AllowOnlyFipsAlgorithms)
        {
            return new SHA256CryptoServiceProvider();
        }

        return new SHA256Managed();
    }
}
like image 127
Soldarnal Avatar answered Oct 03 '22 08:10

Soldarnal


The problem appears to be with the Microsoft.AspNet.Web.Optimization NuGet package. After downgrading the version from 1.3.0 to 1.1.0, everything seems to be working fine.

Link to blog post on codeplex which mentioned the same issue

like image 42
Bhargava Avatar answered Oct 02 '22 08:10

Bhargava


As the answers above did not help me (unsure of the consequences), I found a workaround for this issue.

The problem is, as stated already here, that, when you send a ?v on the query string and the value does not match the actual hash, it will return no-cache.

Not sending anything at all is not an option (cache may never expire). Sending a cache busting parameter is not an option either. If you do so and you have multiple instances, you may cache the wrong value during the deploy (if you don't remove from the load balancer the old instances).

To fix this issue, just set UseCdn to false and change the following during the bundle configuration:

Scripts.DefaultTagFormat = string.Format(@"<script src=""{0}{{0}}""></script>", CdnRoot);

Hope, I've helped.

like image 27
Walter Macambira Avatar answered Oct 03 '22 08:10

Walter Macambira