ctrlplusb / react-universally

A starter kit for universal react applications.
MIT License
1.7k stars 244 forks source link

Bundle fails to load after server restart #511

Closed tijs closed 6 years ago

tijs commented 6 years ago

In the current setup it seems the nonce used for the CSP setup is being re-created for each server restart. The effect of this is that people who navigate the site with a bundle created before the restart suddenly find themselves with an invalid javascript file (according to the Content Security Policy) after the restart of the server. This is not ideal so i was wondering if anyone else has run into this and perhaps found a fix?

I imagine it's possible to invalidate the cached bundles when the server has restarted somehow so the clients fetch new bundles which would then have the new nonce and would not be blocked by the CSP rules. But i'm not quite sure how i can force the client cache to clear just on server restarts.

tijs commented 6 years ago

After some more testing it seems this is not CSP specific. Even without CSP whenever we push a new production build to the server browsers that have any old JS in cache will break until they do a hard refresh. Nobody else is experiencing this issue?

ctrlplusb commented 6 years ago

Hey @tijs this is a super worrying issue. It could possibly be related to the service worker configuration. Is it possible for you to try disabling the service worker and then try again?

diondirza commented 6 years ago

This probably occurs when sw.js file still load from cache, try to make some cache control middleware that make sw.js never being cached. I have this configured in my server

{
    regex: /sw.js$/i,
    value: 'no-store, no-cache, must-revalidate',
},
tijs commented 6 years ago

@ctrlplusb @diondirza ok in playing around with that i might have stumbled on the solution which was probably specific to our project settings. We had already disabled service workers in a quest to resolve this and later also tried running without CSP (see original issue). In removing CSP i also removed the code that set a nonce and included it for the serverHTML. On a whim i just reintroduced the nonce and that seems to fix the cache issue? Am i seeing things or could that indeed be related? Would the nonce perhaps break the caching of the index.html page therefore fixing the issue? Sorry if this a vague issue but it's rather hard te reproduce exactly.

diondirza commented 6 years ago

As per my experience, nonce doesn't affect cache issue, nonce id is unique because its uuid and regenerated per server request, so you could try request with 2 different browser and see if the nonce id generated is different. With nonce u have guard your website so all scripts that being used on your website is authentic (prevent XSS attack), it make sure the script was generated from your server. The moment you disable CSP and nonce you have make a security hole for your site. The moment you want to remove nonce is when you want to install script plugin like GTM that generate script tag dynamically inside their script.

ctrlplusb commented 6 years ago

@tijs I am just guessing here but it could possibly be an issue with the caching configuration for your webhost/proxy of some sorts. The nonce is unique per request and is injected into the output of the html (attached to the scripts) in order to provide validation of their authenticity. It could be in this case that the html is now different per request and that a filecheck sum value is therefore different and then your respective web host/proxy is invalidating the cache it has for the index.html file. Could you check the headers for the response of the index.html and see what the configuration is for the caching of it?

diondirza commented 6 years ago

I have had an issue like you, it's because service worker. Because in this boilerplate our service worker not using cache busting filename (hashed-script). So the moment I upload new version of my bundle it's still load the old one, because service worker not being updated (old sw.js file being served by browser). You could try to open sw.js that being served to your browser to check whether it had point to new bundle version. After I never cache sw.js file, now all should be works fine. Or you could add versioning to your sw file.

tijs commented 6 years ago

@ctrlplusb The current headers look like this:

Connection:keep-alive
Content-Encoding:gzip
Content-Type:text/html; charset=utf-8
Date:Mon, 23 Oct 2017 10:34:48 GMT
ETag:W/"11d222-0h0nVke0P7tQYrCkhecf0bYAKzo"
Server:nginx/1.10.3
Strict-Transport-Security:max-age=31536000; includeSubDomains
transfer-encoding:chunked
Vary:Accept-Encoding
X-Content-Type-Options:nosniff
X-Download-Options:noopen
X-Frame-Options:SAMEORIGIN
X-XSS-Protection:1; mode=block

No cache headers except the ETag. So maybe that was coincidence.

For the JS files the cache headers are as follows:

Accept-Ranges:bytes
Cache-Control:public, max-age=31536000
Content-Encoding:gzip
Content-Type:application/javascript; charset=UTF-8
Date:Mon, 23 Oct 2017 09:57:46 GMT
ETag:W/"1932a8-15f48a89924"
Last-Modified:Mon, 23 Oct 2017 09:55:29 GMT
Server:nginx/1.10.3
Vary:Accept-Encoding
X-Content-Type-Options:nosniff
X-Download-Options:noopen
X-Frame-Options:SAMEORIGIN
X-XSS-Protection:1; mode=block
tijs commented 6 years ago

@diondirza we did run into that issue with sw.js too and simply turned off web workers to fix that for now. You created a cache exception for your sw.js file your saying? Or did you implement versioning for it somehow?

tijs commented 6 years ago

@diondirza regarding CSP: yes i know thats not ideal security wise but one problem at a time right :) we'll turn it back on before we release if we get this issue sorted.

diondirza commented 6 years ago

I leave it with default handle of offline plugin versioning for now, and just add cache control middleware in the server. Here is my code for cache middleware

function cacheHandler(req, res, next) {
  const caches = [
    {
      regex: /.(flv|ico|pdf|avi|mov|ppt|doc|mp3|wmv|wav|woff)$/i,
      value: `public, max-age=${getSeconds('1y')}`,
    },
    {
      regex: /.(jpg|jpeg|png|gif|swf|svg)$/i,
      value: `public, max-age=${getSeconds('30d')}`,
    },
    {
      regex: /.(txt|xml)$/i,
      value: `public, max-age=${getSeconds('3d')}`,
    },
    {
      regex: /.(js|css)$/i,
      value: `max-age=${getSeconds('1y')}`,
    },
    {
      regex: /.(html?)$/i,
      value: 'max-age=0, no-store, no-cache, must-revalidate',
    },
    {
      regex: /sw.js$/i,
      value: 'no-store, no-cache, must-revalidate',
    },
  ];

  caches.forEach((cache) => {
    if (req.url.match(cache.regex)) {
      res.setHeader('Cache-Control', cache.value);
      return true;
    }
    return false;
  });

  next();
}
tijs commented 6 years ago

@diondirza hmm i like that, nice and simple solution.

I will close this issue btw. I guess i need a better defined test case for when this fails. As it is it seems to work so when this issue crops up again i will open a new (better) issue definition.

Thanks for the help so far!