How to: Deep link into an Angular SPA on IIS

When you create a single page application (SPA) with Angular (or other SPA-frameworks, for the matter), you might want to leverage the power of the web and allow a deep link directly to a certain route within your application.

For most, the easy way is to address routes in your application using the so called hash location strategy. This means the URL within your app is separated by a hash and will look like this: https://your.domain/app#route/to/component. However, the hash in the URL actually has a different meaning and should position the browser to a specific anchor (or fragment, as it is called in Angular) on the currently shown page.

Fragment in routing links
Fragment in routing links

In the optimal case we mostly would like to use an semantically more correct URL like this https://your.domain/app/route/to/component#fragment.

This, however, brings up a different issue.

Why do we need the # for a deep link in the first place?

Now, why do we need to resort to the hash in the url at all?

The reason is, that the part in front of the hash is treated as the path to the actual page, and the browser will request exactly that. Our example URL would request the file app/route/to/component from the web server, and this will usually be answered with a HTTP 404 (Not found) status code: the web server does not have a file at this location to deliver. In fact, this route only exists within our client-side application, which is not yet available.

For the application to be available, the browser first needs to load the entry point of our SPA (the index.html), then load the required JavaScript and other required resources, start up the application and finally evaluate the URL to show the correct component to the user.

The solution for an easy deep link

The solution to this is actually pretty simple: We tell the web server to deliver the actual application entry point (our index.html) to the web browser.

The first idea is sending a redirect to the index.html file (using HTTP 301 or 302 responses). This would, however, a) require a second round trip and b) actually change the URL in the browser. If we did this, the SPA would not know of the route in the URL anymore, and we would miss our goal.

Instead, we tell our web server to directly deliver the contents of the index.html file instead of a 404.

Configure IIS to make it so

In our case we host the static files of our SPA on an IIS (Internet Information Server). We need to configure our IIS to deliver the index.html whenever it receives a request it cannot serve otherwise.

For this to happen, we install the URL Rewrite 2.0 extension into IIS. Follow the link and download the installer. It will use the Web Platform Installer to download and install the module into IIS.

Create the basic rule

Now, we need to configure the URL rewrite module to do what we want. In IIS Manager, go to the web site where the SPA is served from and double click on URL rewrite.

We want to ‘Add Rule(s)…’ and create a new rule for the redirect. I called mine ‘redirect all requests’, but you can use whatever name you like to.

In the ‘Match URL’ box, we want to redirect to the index.html whenever we hit any URL. So the ‘Requested URL’ should be set to ‘Matches the Pattern’ and we want to use regular expressions. The pattern we want to use is ^(.*)$, which just means: Match anything from the beginning to the end. We also check the ‘Ignore case’ checkbox.

Match URL rule for a deep link in a SPA
Match URL box

For the moment we skip the ‘Conditions’ and ‘Server Variables’ box and go right to the end to the ‘Action’ box.

We set the ‘Action type’ to ‘Rewrite’ and the ‘Rewrite URL’ to /index.html. We also check the box ‘Append query string’ to preserve the rest of the URL and then check the box ‘Stop processing of subsequent rules’ to prevent the IIS from doing too much work.

Action for a deep link in a SPA
Action box

Tweak the rule for real work usage

Now, this configuration will simply rewrite all requests to our web application and will deliver the contents of our ‘index.html’ file every time. This is not what we want though, as the index.html file will reference some other JavaScript files, images, CSS files etc. from our web server, and we should at least deliver these files 😉

For that, we go back to the ‘Conditions’ box.

Conditions box
Conditions box

First, we ‘Add’ a new condition.

The ‘Condition input’ should be set to {REQUEST_FILENAME}, which is a IIS variable and points to the actual requested file. We set the ‘Check if input string’ drop down to ‘Is Not a File’ and save this condition.

Existing file exclusion
File exclusion

This will prevent IIS from rewriting a URL that points to an existing file, which is exactly what we want: Deliver the existing file instead of the index.html.

Exclude certain paths

In my special case I configured a sub-application in my web site to serve the backend API from the folder ‘/api’. So I do not want the IIS to rewrite requests to the ‘/api’ folder, too.

For that, I added another condition rule that says if the ‘Condition input’ {REQUEST_URI} ‘Does Not Match the Pattern’ /api(.*)$ (with ignore case). This will prevent all requests that start with ‘/api’ from being rewritten.

API path exclusion
API exclusion

Since I now have multiple rules, I set the ‘Logical grouping’ in the ‘Conditions’ box to ‘Match all’.

Make the rules deployable

We do not want to configure this every time we add a new web site. Also, ISS is a nice player and writes all rules into the ‘web.config’ file. Therefore my use case ended up resulting in this little file of elegant IIS rewrite magic:

<?xml version="1.0" encoding="UTF-8"?>
                <rewriteMap name="^(.*)$" />
                   name="redirect all requests"
                    <match url="^(.*)$" />
                    <conditions logicalGrouping="MatchAll">
                    <action type="Rewrite" url="/index.html" />

Finally we can leverage this to simply make this web.config file a part of our Angular web application. My Angular application uses the angular-cli tooling, and so I just added the ‘web.config’ file to my .angular-cli.json file in the app.assets array.


If you want to deep link into your Angular SPA, you can avoid using the hash location strategy. By configuring your web server to deliver the application entry point file instead of a 404 Not found it is possible to bootstrap your application at any route within your application.

This post shows how to achieve that using the URL rewrite module in IIS, but the same concept applies to other web servers capable of rewriting urls.

Why the IIS overwrites your vary-Header with ‘Accept-Encoding’

I spent some time today searching for a bug. Turned out it was a nasty one.

If you have a single line of code, being there for a single purpose, you usually expect this code to work.

My case: Setting a HTTP header on a response to a normal HTTP request. One probably would think this is a normal task. I wanted to set a header to control caching. More specifically, the ‘vary’ header, as the result of the request is dependent upon a specific header sent with the request.

Besides the line setting the vary header, I had two other lines setting cacheability to public and setting the absolute expiration for that specific response.

As expected, the expiration was set and the cacheability was set to public. But surpinsingly, the vary header always was ‘Accept-Encoding’, ignoring the value I specifically set. I tried setting the header via the cache class on the response, directly on the response, in my controller, in the EndRequest event on the application.. everything failed. I set another header on the same places, all of them worked. As soon as I wanted to change or even remove the ‘vary’ header, I had no chance. It always was sent with the value ‘Accept-Encoding’.

So I searched in my whole code base if there has been some tampering with the header. No result too. And then the nasty search began.

To make this post a bit shorter, I discovered that this is a bug in the web server that hits you as soon as you enable dynamic content compression. In this case, IIS overwrites your vary header with ‘Accept-Encoding’.

Of course it makes sense to *add* that value to the header, because when a GZip’ed response is cached and returned to a client that does not accept gzip as encoding, the client would receive a compressed response it can not decode. What does *not* make sense though, is to overwrite pre-existing vary-headers, as they usually are being set for a reason too 😉

That bug in IIS was reported in August 2012:

It was not until November 2013 until it was fixed: (at least, in November the binaries for this hotfix were built), it seems the KB article and hotfix was officially released just a few days ago.

The good: I discovered what the issue was and was able to direct my co workers to the best solution (patching the affected systems).
The bad: I spent almost half a day for researching and reproducing that issue until I found the solution.
The ugly: In fact, I would never have expected such a massive bug in a product like IIS, which I honestly consider rock-stable as a platform you can easily depend on.