52

I'm hosting an SPA on firebase where almost all paths get rewritten to index.html. I'm using webpack hash based cache busting, so I want to always prevent caching of my index.html but not any other files. I'm finding it surprisingly difficult to do so. Specifically, my file layout looks like this

/
├── index.html
├── login.html
├── js
│   ├── login.ba22ef2579d744b26c65.bundle.js
│   └── main.6d0ef60e45ae7a11063c.bundle.js
└── public
    └── favicon-16x16.ico

I started naively with "sources": "index.html" before reading this quote from the docs.

Each definition must have a source key that is matched against the original request path regardless of any rewrite rules using glob notation.

Ok, so instead of a simple glob that specifies the files I want these headers on, I guess I need one on paths. Since most paths redirect to index.html, I need a glob that excludes all the paths I do not want to put these headers on.

For reference, my firebase.json hosting section looks like this:

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    "cleanUrls": true,
    "trailingSlash": false,
    "headers": [
      {
        "source": <<<WHAT-GOES-HERE?>>>,
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          },
          {
            "key": "Pragma",
            "value": "no-cache"
          },
          {
            "key": "Expires",
            "value": "0"
          }
        ]
      }
    ]
  }
}

So, to give some examples that redirect to index.html and should not be cached

mysite.com  
mysite.com/  
mysite.com/foo/bar/baz  
mysite.com/index.html 

Note: I could live if that last one got cached since it doesn't get used in practice.

And the things that do not redirect to index.html and should not be cached

**/*.* (ideally excluding index.html)
mysite.com/login  

The closest I've gotten on my own is **/!(login|*.*) which works for almost everything listed above, but inexplicably does not work on mysite.com or mysite.com/. Those 2 pages are not getting matched by this glob and I cannot figure out why.

zevdg
  • 1,091
  • 2
  • 9
  • 20
  • 2
    Have you tried additionally adding a rule for `/index.html`? – Michael Bleigh Feb 02 '18 at 21:19
  • Adding a 2nd section with a source glob of simply "/" seems to do the trick for me. I'll leave the question unanswered though as it would be much more elegant if this could be done in a single glob/section. – zevdg Feb 06 '18 at 01:21

3 Answers3

121

Here is the config that I'm using. The logic is to use cache for all static files like images, css, js etc.. For all others, i.e "source": "/**" set cache as no-cache. So for all other files, that maybe example.com, example.com/index.html, example.com/about-us, example.com/about-us.html cache will not be applied.

{
  "hosting": {
    "public": "dist",
    "headers": [
      {
        "source": "/**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          }
        ]
      },
      {
        "source":
          "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|woff2|font.css)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=604800"
          }
        ]
      }
    ],
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
  }
}
Gijo Varghese
  • 11,264
  • 22
  • 73
  • 122
  • 1
    added webp to mine :) – Rusty Rob Sep 26 '18 at 03:23
  • @robertking thanks for your suggestion! I've updated the answer – Gijo Varghese Sep 26 '18 at 03:45
  • you can also add woff2 – alexkom Apr 29 '19 at 08:34
  • @alexkom Done! Thanks :) – Gijo Varghese Apr 29 '19 at 10:29
  • 1
    @GijoVargehese while we're at it, wasm probably also makes sense – Alex Suzuki Jul 04 '19 at 05:51
  • 1
    How would you ensure that `service-worker.js` is not cached as well? – Konstantin Tarkus Jul 16 '19 at 14:20
  • Here is service worker https://stackoverflow.com/a/46667978/7911479 – jthegedus Apr 25 '20 at 02:55
  • 2
    I'm finding with this approach it adds 300-400ms to index.html page load time from CDN (like the CDN itself stops caching it at all), even when comparing hard refreshes to hard refreshes. Is there any way to get the best of both worlds here, invalidating the cache when the file changes, always having clients grab the latest from the CDN? – Brian Jordan Aug 11 '20 at 04:09
  • 4
    Update: answering my own question (would love to hear if this is a good or bad idea) — using a Cache-Control value of: `max-age=0, s-maxage=604800` seems to get my desired behavior of instant client updates on new page contents, but still caching at the CDN level – Brian Jordan Aug 11 '20 at 04:22
  • i am not able to understand where to add this, sorry if beginner question – Sanskar Tiwari Oct 31 '20 at 05:14
  • @SanskarTiwari add to `/firebase.json` – dagalti Nov 19 '20 at 04:33
  • @BrianJordan what is you update your page and a user had the old version and refreshes it before `604800` seconds? Wouldn't he get a stale response? – cbdeveloper Feb 08 '21 at 19:32
  • @cbdeveloper the Manage cache behavior doc states: "Any requested static content is automatically cached on the CDN. If you redeploy your site's content, Firebase Hosting automatically clears all your cached static content across the CDN until the next request." So if you push a new update, it should clear the CDN's cache, then on next request will start a new cache for 604800 seconds. – JM_24 Aug 24 '21 at 04:19
  • 1
    Be wary with this configuration, if your app also includes a service worker (we had a P1 issue escalated within the firebase org, and this is what we found). You need to ensure that the service worker is NOT cached, and firebase hosting works using a "last rules wins", so you need to ensure your service worker rule is last. – jules testard Mar 26 '22 at 10:07
4

Be wary with the configuration, if your app also includes a service worker (we had a P1 issue escalated within the firebase org, and this is what we found). You need to ensure that the service worker is NOT cached, and firebase hosting works using a "last rules wins", so you need to ensure your service worker rule is last. Prefer something like this:

{
  "hosting": {
    "public": "dist",
    "headers": [
      {
        "source":
          "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|woff2|font.css)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "max-age=604800"
          }
        ]
      },
      {
        "source": "/**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          }
        ]
      },
      {
        "source": "/service-worker.js",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache, no-store, must-revalidate"
          }
        ]
      }
    ],
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"]
  }
}

Especially because your service worker might do pre-caching, so even though your index.html will not be cached at the CDN level, and HTTP request would get a new copy, with the pre-caching of the service worker, you might still serve an old copy of the index.html

jules testard
  • 193
  • 1
  • 9
1

I recently ran into this issue as well, and found that using a source glob of /**/!(*.*) works nicely, as it will target any subdirectories where the ending does not contain a ., which essentially means it will only match routes (i.e., /, /login, /account/edit, etc.). Then I don't need to write an exclusion for every specific file extension.

I hope this helps someone (and makes your configs a bit more sane)!

{
  "hosting": {
    "public": "build",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ],
    "headers": [
      {
        "source": "/**/!(*.*)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "no-cache"
          }
        ]
      },
      {
        "source": "**/*.chunk.@(js|css)",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      }
    ]
  }
}

Also note, I'm only aggressively caching JS and CSS files with .chunk. in them (this should be changed to .bundle. in the case of the OP).

atdrago
  • 295
  • 4
  • 16