Description
Background
My app is an Angular SPA. As part of a production build, Angular's CLI gives me "versioned" JS/CSS files containing a hash (e.g. main.[hash].js
) and these files are referenced inside index.html
.
If the user's browser (or any intermediary CDN) uses a cached index.html
that is out of date, it may refer to old JS files (e.g. main.[oldhash].js
) which no longer exist, resulting in them not seeing the latest version of the app. As such, caching needs to be configured to ensure that a cached index.html
can only be used if it is first validated as being up to date (e.g. by validating the etag)
Is your feature request related to a problem? Please describe.
There does not seem to be an easy way to configure what headers should be attached to responses that are served due to the navigationFallback
. This makes it hard to get fine grained control of caching headers for an SPA which requires client-side routing.
For my test I set up a Static Web App in combination with Azure Front Door. To give an example, take the following staticwebapp.config.json
:
{
"routes": [
{
"route": "/index.html",
"headers": {
"Cache-Control": "no-cache",
}
},
{
"route": "/",
"headers": {
"Cache-Control": "no-cache",
}
}
],
"navigationFallback": {
"rewrite": "index.html",
"exclude": ["/assets/*"]
},
"globalHeaders": {
"Cache-Control": "public, max-age=3600",
},
...
}
With this configuration:
a) requesting mysite.com/index.html
serves index.html with no-cache
response header (due to the first route-specific config)
b) requesting mysite.com/
serves index.html with no-cache
response header (due to the second route-specific config)
c) requesting mysite.com/somethingelse
serves index.html with public, max-age=3600
response header (navigationFallback applies, but it uses the headers from globalHeaders
and not the index.html
route config)
By the nature of it being a fallback, there is no obvious pattern that could be used in the routes
section of the config to capture the "fallback" paths as this could be any string except one where a file exists on disk with that name.
Describe the solution you'd like
I'd like all three of the above situations to serve the no-cache
header to ensure that the user always receives the latest index.html (because it would be forced to validate the etag). Any requests to specific files that do exist (such as somescript.[hash].js) should use the globalHeaders
(webpack bundled JS files with a hash in the name are suitable for more aggressive caching).
As far as I can gather, routes are not applied on NavigationFallback by design, but this means it is hard to apply specific configuration to them in "scenario C" above.
My suggestion would be to add the ability to specify headers
as part of the navigationFallback
section.
"navigationFallback": {
"rewrite": "index.html",
"exclude": ["/assets/*"],
"headers": { <-- note: this is my suggestion, not something that is currently supported
"Cache-Control": "no-cache",
}
},
Describe alternatives you've considered
My current workaround is to negate the logic (conservative caching by default, more aggressive caching for specific routes with a wildcard pattern). This seems to work, but is fiddlier to set up & understand and has the potential to result in new files being cached too heavily if happen to match the pattern.
My workaround works as follows:
- sets
no-cache
as the global default header, such that all requests need to validate the etag before using a cached version - for any files inside the
assets
directory (containing assets which may be subject to change over time), setno-cache
header. Becauseroutes
are matched in order and it stops at the first match, they will not be affected by any subsequent route parameters in the array. - for specific file patterns outside of
assets
such as JS/CSS files (which the Angular/Webpack build process should have added hashes for, making them suitable for more aggressive caching), setpublic, max-age=3600
header so that they can potentially avoid network requests entirely
{
"routes": [
{
"route": "/assets/*",
"headers": {
"Cache-Control": "no-cache"
}
},
{
"route": "/*.{js,css,ttf,woff,eot,svg}",
"headers": {
"Cache-Control": "public, max-age=3600"
}
}
],
"navigationFallback": {
"rewrite": "index.html",
"exclude": ["/assets/*"]
},
"globalHeaders": {
"Cache-Control": "no-cache"
},
...
}