The idea of minimizing and combining multiple script and style files into one file has been popular among web developers for quite some time. With the 4th version of ASP.NET MVC Microsoft introduced a mechanism (called bundles) that allow .NET developers to automate and control this process. Although bundles are quite easy to configure and use, they might sometimes not behave as expected. In this post I’m going to acquaint you with bundles internals and present you ways to troubleshoot problems they may generate.
Bundles Architecture
To examine bundles, let’s create a default ASP.NET MVC project in Visual Studio 2013. This project should have a BundleConfig.cs file in App_Start folder with some bundle routes defined, e.g.:
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
...
}
}
After the above code is called from Global.asax on Application_Start
event, a new route will be created and a request to http://localhost:8080/bundles/jquery.js?v=JzhfglzUfmVF2qo-weTo-kvXJ9AJvIRBLmu11PgpbVY1 will render a minimized version of jquery (unless the <compilation>
tag does not have the debug
attribute set to true
). To understand how it works, let’s have a look at how bundles interact with the ASP.NET pipeline. As we know, requests coming to an ASP.NET application need to be served by a handler. At first, a default handler is assigned by IIS based on a mask (handlers tag in applicationhost.config). Then the request is processed by all the HTTP modules defined in the configuration files (in the integrated mode, a precondition must also be fulfilled). Each module has a chance to change the already assigned handler. Finally, the chosen handler processes the request. Starting from .NET 4, there is also a possibility to inject HTTP modules into the ASP.NET pipeline dynamically from our application code. For this purpose, we need to add a PreApplicationStartMethodAttribute
attribute to our assembly. When HTTP runtime detects an assembly with such an attribute, it will execute a method the attribute defines before the application starts. As we are examining bundles, let’s take as an example System.Web.Optimization.dll assembly. It has the following attribute set:
[assembly: PreApplicationStartMethod(typeof (PreApplicationStartCode), "Start")]
And the PreApplicationStartCode
class looks as follows:
[EditorBrowsable(EditorBrowsableState.Never)]
public static class PreApplicationStartCode
{
private static bool _startWasCalled;
public static void Start()
{
if (PreApplicationStartCode._startWasCalled)
return;
PreApplicationStartCode._startWasCalled = true;
DynamicModuleUtility.RegisterModule(typeof (BundleModule));
}
}
Notice that the above code registers a new BundleModule
in the ASP.NET pipeline:
public class BundleModule : IHttpModule
{
...
private void OnApplicationPostResolveRequestCache(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication) sender;
if (BundleTable.Bundles.Count <= 0)
return;
BundleHandler.RemapHandlerForBundleRequests(app);
}
...
}
Remapping happens only if a static
file with a name equal to our bundle does not exist:
internal static bool RemapHandlerForBundleRequests(HttpApplication app)
{
HttpContextBase context = (HttpContextBase) new HttpContextWrapper(app.Context);
string executionFilePath = context.Request.AppRelativeCurrentExecutionFilePath;
VirtualPathProvider virtualPathProvider = HostingEnvironment.VirtualPathProvider;
if (virtualPathProvider.FileExists(executionFilePath) ||
virtualPathProvider.DirectoryExists(executionFilePath))
return false;
string bundleUrlFromContext = BundleHandler.GetBundleUrlFromContext(context);
Bundle bundleFor = BundleTable.Bundles.GetBundleFor(bundleUrlFromContext);
if (bundleFor == null)
return false;
context.RemapHandler((IHttpHandler) new BundleHandler(bundleFor, bundleUrlFromContext));
return true;
}
After a BundleHandler
is chosen to process a given request, it creates a context for bundle operations and examines the BundleTable
in search for a bundle that should be sent to the browser. Bundles are cached by their hash so subsequent calls for the same bundle perform much faster than the first one.
IIS Configuration for Bundles
For simplicity’s sake, I will focus only on Integrated Pipie in IIS7+. You need to be sure that ASP.NET handler is called for your bundle requests, otherwise they won’t be served. If you are using URLs in the form of /bundlename?v=bundlehash
, the default handler configuration in IIS (presented below) should be good.
<handlers>
...
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*."
verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TransferRequestHandler"
preCondition="integratedMode,runtimeVersionv4.0" />
<add name="StaticFile" path="*" verb="*"
modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule"
resourceType="Either" requireAccess="Read" />
</handlers>
And in the IIS Failed Request Trace, you should see the following events (I marked in red the ones that are related to bundles):
Notice that the ExtensionlessUrlHandler-Integrated-4.0
handler assigned at first by IIS is then replaced by System.Web.Optimization.BundleHandler
. We already know that this replacement is ordered by System.Web.Optimization.BundleModule
on the RESOLVE_REQUEST_CACHE
notification (marked in red on the image).
Troubleshooting Problems
So far, we examined bundles internals and their correct interaction with the ASP.NET (IIS) pipeline. But what if things go wrong and instead of seeing nicely compacted JavaScript, you receive 404 HTTP response? We had such a problem in production in one of our applications. Just after deploying a new version of this application, bundles were never working (returning 404 code). The only fix we found was to restart the application pool after a deploy. As you can imagine, it was less than desirable solution so I started investigating the root cause of our problem. During tests, I found out that this problem was appearing only when the application was interrupted with requests during deployment (by, for example, our load balancer which was checking if the application is responding). Example JavaScript bundle in our application had the following path: bundle/Site.js?v=77xGE3nvrvjxqAXxBT1RWdlpxJyptHaSWsO7rRkN_KU1. Did you notice a subtle difference between this url and the one from the ASP.NET example application? Yes, the .js EXTENSION! This small part of the url changed dramatically the way IIS handled requests for bundles. Till application was ready (fully deployed), IIS tried to serve them using StaticFileHandler
(which was in accordance to its handlers mask configuration). Also, it appears that IIS caches which modules were run for a given URL. Thus, even when our application was ready to serve the bundle requests, IIS didn’t run System.Web.Optimization.BundleModule
on them. We eventually removed the .js extension from the bundles URL. Another solution might have been to change the mask for the ExtensionlessUrlHandler-Integrated-4.0
to *
. This would force IIS to run the managed module for all the requests to the application.
If you would like to check which files were included into a bundle, you may tamper the request (using for example fiddler) by modifying the User-Agent
header to Eureka/1
, example request:
GET http://localhost:8080/Content/css?v=WMr-pvK-ldSbNXHT-cT0d9QF2pqi7sqz_4MtKl04wlw1 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/css,*/*;q=0.1
If-Modified-Since: Sat, 15 Feb 2014 15:52:46 GMT
User-Agent: Eureka/1
Referer: http://localhost:8080/
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8,pl;q=0.6,fr-FR;q=0.4,fr;q=0.2
and the response:
HTTP/1.1 200 OK
Cache-Control: private
Content-Type: text/css; charset=utf-8
Vary: Accept-Encoding
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?YzpcdGVtcFxidW5kbGUtdGVzdFxDb250ZW50XGNzcw==?=
X-Powered-By: ASP.NET
Date: Sat, 15 Feb 2014 22:12:52 GMT
Content-Length: 14076
/* Bundle=System.Web.Optimization.Bundle;Boundary=MgAwADcANgAwADIAMwAyADUA; */
/* MgAwADcANgAwADIAMwAyADUA "~/Content/site.css" */
html {
background-color: #e2e2e2;
margin: 0;
padding: 0;
}
...
Summary
I hope that this post helped you better understand ASP.NET bundles. They are a great mechanism to automatically group and minimize script and style files in your application. And if you ever encounter any problems with them, remember about IIS Failed Request Trace and Eureka/1 user agent.
Filed under: CodeProject, Diagnosing ASP.NET