54

I have a single page application (angular-js) which is served through IIS. How do I prevent caching of HTML files? The solution needs to be achieved by changing content within either index.html or the web.config, as access to IIS through a management console is not possible.

Some options I am currently investigating are:

IIS is version 7.5 with .NET framework 4

Community
  • 1
  • 1
Andrew
  • 5,525
  • 5
  • 36
  • 40

7 Answers7

65

Adding the following into web.config solution worked across Chrome, IE, Firefox, and Safari:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <location path="index.html">
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <add name="Cache-Control" value="no-cache" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>

</configuration>

This will ensure that the that Cache-Control header is set to no-cache when requesting index.html.

Mohamad Shiralizadeh
  • 8,329
  • 6
  • 58
  • 93
Andrew
  • 5,525
  • 5
  • 36
  • 40
  • 31
    I think this only works when hitting a url that has directly index.html in it.. all of the requests in an SPA have virtual urls and don't map to real location paths. What can be done with that? – Michail Michailidis Aug 27 '15 at 17:50
  • 2
    @MichailMichailidis For a SPA you probably have a rule to rewrite your urls to '/'? What if you add that path as well? – Jason Goemaat Sep 17 '18 at 00:35
  • 1
    While this answer is technically accurate, I think it worth pointing out that `Cache-Control: no-cache` doesn't disable cache but rather forces the client to check the origin server. As such it works concertedly with the `etag` header or possibly the unreliable `last-modifed` header, but it does not outright disable cache. From MDN for `no-cache`: "Caches must check with the origin server for validation before using the cached copy." In other words, the browser may still use a cached copy if a 304 isn't returned by the server. – benbotto Jan 30 '20 at 21:19
  • to address concerns of @MichailMichailidis ensure your catchall rewrite rule is `` or see [this link](https://medium.com/@mateioprea/setting-up-a-react-app-with-react-router-in-iis-71cb86aee376) for full `Web.config` – wal May 05 '20 at 02:06
29

For .NET Core, I used the following.

        app.UseStaticFiles(new StaticFileOptions
        {
            OnPrepareResponse = context =>
            {                   
                if (context.File.Name == "index.html" ) {
                    context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
                    context.Context.Response.Headers.Add("Expires", "-1");
                }
            }
        });

Credit to How to disable browser cache in ASP.NET core rc2?

ttugates
  • 5,818
  • 3
  • 44
  • 54
  • I'm not very familiar with React (just trying to fix our cache problem), but for me, when debugging, with a breakpoint on the above `if` statement, the breakpoint is not hit for `index.html`. I tried putting the code in both `app.UseStaticFiles` and `app.UseSpaStaticFiles`. The second one is hit for `favicon.ico` and `manifest.json`, located in the same folder as `index.html`. Any ideas? – Kjell Rilbe Oct 22 '19 at 11:46
  • @Kjell Rilbe, if breakpoint is not being hit for `index.html`, verify in browser console its being requested from the server and not browser cache. If the browser is caching, then you would want to address scenario of `index.html` files that previously did not have cache control headers and thus the browser isn't aware of server's change. At the moment I don't recall how to address this(append useless query string to path, clear browser cache once manually might be helpful for debugging). Good luck. – ttugates Oct 22 '19 at 12:40
  • 2
    Thanks, I'll check that once more (think I already did, but to make sure). The breakpoints get hit only twice and that's for the files i mentioned, although the prowser actually seems to fetch a number of other resources. The page `index.html` is probably fetched as default document/root URL without path or file specified, which is why I put a breakpoint on the `if`, to see what `context.File.name` actually comes in in that scenario. But... notthing... – Kjell Rilbe Oct 23 '19 at 06:34
  • @KjellRilbe if you are using `UseSpa(spa =>` then you should use `spa.DefaultPageStaticFileOptions`, note, it won't fire in dev server when using `UseAngularCli` but if you comment out dev server then build the client app manually, then run it, it will hit the breakpoint. Oh and you don't need the `if` statement, `DefaultPageStaticFileOptions` only fires for index.html. – Glass Cannon Sep 18 '22 at 13:33
13

The main premise for SPA is Never cache index.html

Following the MDN recommendations to prevent caching, we must add the Cache-Control HTTP header with the no-store, max-age=0 value to the resource (the index.html file in our case).

Why no-store instead no-cache?

Whith no-store, the resource is not stored anywhere. With no-cache, the resource may be stored, but it should be validated with the server by the store before use it.

Why max-age=0?

Force to clear pre-existing valid cache responses (no-store doesn't).

In IIS we can manage our app cache configuration through the web.config file. Here is a complete web.config file (must be located in the root directory of our application) that includes the cache configuration for the index.html file as well as the routes configuration (I have added the SPA routing and the HTTP redirection to HTTPS as examples):

<configuration>
  <location path="index.html">
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <add name="Cache-Control" value="no-store, max-age=0" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
  </location>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="HTTP to HTTPS" enabled="true" stopProcessing="true">
          <match url="(.*)" />
          <conditions>
            <add input="{HTTPS}" pattern="^OFF$" />
          </conditions>
          <action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" />
        </rule>
        <rule name="SPA Routes" enabled="true" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/index.html" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>
Rafael Neto
  • 1,036
  • 10
  • 16
9

When serving your html files, you can append a random query string. This will prevent the browser from using the old versions even if the file is in the browser cache.

/index.html?rnd=timestamp

The other option is to add the no-cache setting at IIS level. This adds Cache-Control: no-cache in the response which tells browsers to not cache the file. It works from IIS 7 onwards.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <!-- Note the use of the 'location' tag to specify which 
       folder this applies to-->
  <location path="index.html">
    <system.webServer>
      <staticContent>
        <clientCache cacheControlMode="DisableCache" />
      </staticContent>
    </system.webServer>
  </location>
</configuration>
govin
  • 6,445
  • 5
  • 43
  • 56
  • 1
    Regarding adding a timestamp to the URL - yes this would work but I see this as more of a temporary hack rather than a solution. I don't believe any reputable SPA solutions use this method. – Andrew Oct 28 '14 at 04:34
  • Don't forget your ng routes, css and javascript. The way you do this is web.config for index.html. ?v=1 for everything else e.g. main.css?v=1. – DalSoft Mar 28 '16 at 21:23
  • Mostly a landing page like index.html is not requested by in-app code but by an end-user whom will just specify the address in the browser and won't append a random timestamp so "the other option" is the way to go – Youp Bernoulli Dec 07 '21 at 10:27
9
  <meta http-equiv="cache-control" content="no-cache, must-revalidate, post-check=0, pre-check=0">

Sagar Jadhav
  • 1,111
  • 11
  • 10
7

None of these answers worked for me because, as @MichailMichailidis mentioned, "all of the requests in an SPA have virtual urls and don't map to real location paths", so location path never matches index.html.

I found the solution in this blog post, which links to this answer. Both show you how you can use rewrite module's outboundRules to change the cache-control response header based on a condition.

So this is what my web.config looks like after this configuration:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <rule name="RewriteCacheControlForIndexHtmlFile" 
          preCondition="IsIndexHtmlFile">
          <match serverVariable="RESPONSE_Cache_Control" pattern=".*" />
          <action type="Rewrite" value="max-age=0" />
        </rule>
        <preConditions>
          <preCondition name="IsIndexHtmlFile">
            <add input="{REQUEST_FILENAME}" pattern="index.html" />
          </preCondition>
        </preConditions>
      </outboundRules>
...
Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
1

For my case none of previous answears worked, but I found another way, by filter by response content type.

Just add below to web.config

...
<system.webServer>
    <rewrite>
        <outboundRules>
            <rule name="RewriteCacheControlForHtmlFile" preCondition="IsHtmlFile">
                <match serverVariable="RESPONSE_Cache_Control" pattern=".*" />
                <action type="Rewrite" value="no-store, max-age=0" />
            </rule>
            <preConditions>
                <preCondition name="IsHtmlFile">
                    <add input="{RESPONSE_CONTENT_TYPE}" pattern="html$" />
                </preCondition>
            </preConditions>
...
Damian Kurek
  • 111
  • 6