Chrome Ignoring Cache-Control no-store directive? - javascript

I have a web-app I'm allowing users to add scripts to. These scripts are written in JavaScript and run in the user's browser. When developing new scripts to run locally, I've added a button to the app that allows you to load it from a locally running web server, (i.e. so you'd click on it and enter http://path.to.my.pc:12345/script.js). My app will fetch this script and append to the DOM.
These scripts are assumed to be ES6 modules and Chrome happily handles those, recursively importing correctly.
However when running locally, I also wanted the ability for users to "refresh" as they're developing such that the app will hit their server again to redownload the scripts. Chrome does not seem to want to do this. Specifically, despite the fact that my local test server has specified no-store as Cache-Control, Chrome doesn't care. Even if I cacheBust script.js (i.e.http://blah/script.js?cb=randomInt), this cacheBust parameter is not recursively passed to the imports.
Here's the text of my locally running dev server:
const express = require("express");
const serveStatic = require("serve-static");
const morgan = require("morgan");
function setHeaders(res, path) {
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', '*');
}
const app = express();
app.use(morgan('combined'));
app.use(serveStatic('./', { setHeaders });
app.listen(12345);
Is there something else I can do? I really don't want to force my users to run webpack. The idea is to keep this as simple and stupid as possible so they can just focus on writing their scripts.
Edit Update: Checking 'Disable Caching' in Devtools also does not cause Chrome to actually... not cache.

Some ideas :
Have you prior to everything, do "developer tools > application >
clear storage" to avoid using previous server instance cached ?
Have you tried "Cache-Control:no-cache, no-store" ?
Have you tried on non Chrome browser ?
Have you cleared all in client-side browser (ctr+alt+suppr) or hard
reload (ctrl+shift+r) ?
I've the same need and in my node server i use
const nocache = require('nocache');
app.use(nocache());
app.set('etag', false);
And in client side, my query use only
'Cache-control', 'no-store'

Have you tried implementing ETag's? along with your current headers?
The ETag specifies a version of the resource, if the version changes it should force the browser to revalidate it.

Upload the script to a scripts folder and serve them with cache-control by setting maxAge option. Whenever a user refreshes it will load the file stored in his browsers cache:
app.use('/scripts', express.static('./scripts', { maxAge: 1000 }));
{ maxAge: 1000 } means that a HTTP response will remain in the user browser as a cached copy for the next 1000 seconds before it can be available for reuse.

Related

Ionic/Capacitor React App API Requests returning HTML on iOS

The current behavior:
All API requests to the localhost server are returning the index.html file of the React.js app on the iOS build, but requests work fine on browser and PWA builds.
The expected behavior:
Requests return the intended data (usually JSON).
Details
Typically, API requests go to http://localhost:3000/api/[route]. In the iOS build, they are going to capacitor://localhost/api/[route]
Because the route is returning HTML and not JSON data, I am getting the following error (one of many, as each API route, has the same error) which causes a white screen:
TypeError: undefined is not a function (near '...a.map...')
I tried adding a hostname (location where the production server is hosted) to the capacitor.config.json file in my root directory, but it still fails. Dev API server is running on localhost:3000.
Tried setting the server hostname to "localhost:3000" in the config as well.
"server": {
"hostname": "localhost:3000"
},
"http" is not allowed as a iosScheme as mentioned in the docs for the capacitor config:
Can't be set to schemes that the WKWebView already handles, such as http or https](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler)
Because the site works on every other build other than this iOS build, the issue should be here...
How can I route my requests on the iOS build to direct to the correct location?
Update
If I set the server.url option in the capacitor.config.json file to https://www.website.com, I'm able to pull data from my production server, but when the authorization check returns a 401, the app seems to hang on the splash screen and the only error-like message I get in the console is that the response returned a 401 (as expected)
Device console output:
XCode console output:
The first issue was fixed by setting the server.url parameter in the capacitor.config.json file to the URL of the production server:
"server":{
"url":"https://www.website.com"
}
Second issue was making sure the splash screen hide code from #capacitor/splash-screen was actually deployed onto prod in the index.js file.
import { SplashScreen } from '#capacitor/splash-screen';
setTimeout(() => {
SplashScreen.hide();
}, 2000);
By the little information you have provided I'll try to guess what's wrong.
You are using relative urls instead of absolute urls. If you make a XHR/fetch call to /api/[route], the browser will append the scheme and host name to it and will turn it into capacitor://localhost:3000/api/[route]. That url belongs to the app, not to the web server where you API is running.
You are using localhost, localhost means "this machine", if you run the app in a browser or iOS simulator, localhost is the machine where the browser and simulator and browser are running, and the local server is also running on the same machine, so they don't have any problem accessing it. But if you run on a real device, localhost is the device, and the device doesn't have a server running. You should use the local IP (numbers) of the computer where the server is running, and the device should be connected to the same network for being able to access it.
Capacitor apps are "affected" by CORS, you need special server configurations to allow the connections https://ionicframework.com/docs/troubleshooting/cors.
Looks like you are on the point 1, once you fix it you'll hit point 2 and then probably point 3 after that.

Webpack code splitting: ChunkLoadError - Loading chunk X failed, but the chunk exists

I've integrated Sentry with my website a few days ago and I noticed that sometimes users receive this error in their console:
ChunkLoadError: Loading chunk <CHUNK_NAME> failed.
(error: <WEBSITE_PATH>/<CHUNK_NAME>-<CHUNK_HASH>.js)
So I investigated the issue around the web and discovered some similar cases, but related to missing chunks caused by release updates during a session or caching issues.
The main difference between these cases and mine is that the failed chunks are actually reachable from the browser, so the loading error does not depend on the after-release refresh of the chunk hashes but (I guess), from some network related issue.
This assumption is reinforced by this stat: around 90% of the devices involved are mobile.
Finally, I come to the question: Should I manage the issue in some way (e. g. retrying the chunk loading if failed) or it's better to simply ignore it and let the user refresh manually?
2021.09.28 edit:
A month later, the issue is still occurring but I have not received any report from users, also I'm constantly recording user sessions with Hotjar but nothing relevant has been noticed so far.
I recently had a chat with Sentry support that helped me excluding the network related hypotesis:
Our React SDK does not have offline cache by default, when an error is captured it will be sent at that point. If the app is not able to connect to Sentry to send the event, it will be discarded and the SDK will no try to send it again.
Rodolfo from Sentry
I can confirm that the issue is quite unusual, I share with you another interesting stat: the user affected since the first occurrence are 882 out of 332.227 unique visitors (~0,26%), but I noticed that the 90% of the occurrences are from iOS (not generic mobile devices as I noticed a month ago), so if I calculate the same proportion with iOS users (794 (90% of 882) out of 128.444) we are near to a 0,62%. Still small but definitely more relevant on iOS.
This is most likely happening because the browser is caching your app's main HTML file, like index.html which serves the webpack bundles and manifest.
First I would ensure your web server is sending the correct HTTP response headers to not cache the app's index.html file (let's assume it is called that). If you are using NGINX, you can set the appropriate headers like this:
location ~* ^.+.html$ {
add_header Cache-Control "no-store max-age=0";
}
This file should be relatively small in size for a SPA, so it is ok to not cache this as long as you are caching all of the other assets the app needs like the JS and CSS, etc. You should be using content hashes on your JS bundles to support cache busting on those. With this in place visits to your site should always include the latest version of index.html with the latest assets including the latest webpack manifest which records the chunk names.
If you want to handle the Chunk Load Errors you could set up something like this:
import { ErrorBoundary } from '#sentry/react'
const App = (children) => {
<ErrorBoundary
fallback={({ error, resetError }) => {
if (/ChunkLoadError/.test(error.name)) {
// If this happens during a release you can show a new version alert
return <NewVersionAlert />
// If you are certain the chunk is on your web server or CDN
// You can try reloading the page, but be careful of recursion
// In case the chunk really is not available
if (!localStorage.getItem('chunkErrorPageReloaded')) {
localStorage.setItem('chunkErrorPageReloaded', true)
window.location.reload()
}
}
return <ExceptionRedirect resetError={resetError} />
}}>
{children}
</ErrorBoundary>
}
If you do decide to reload the page I would present a message to the user beforehand.
The chunk is reachable doesn't mean the user's browser can parse it. For example, if the user's browser is old. But the chunk contains new syntax.
Webpack loads the chunk by jsonp. It insert <script> tag into <head>. If the js chunk file is downloaded but cannot parsed. A ChunkLoadError will be throw.
You can reproduce it by following these steps. Write an optional chain and don't compile it. Ensure it output to a chunk.
const obj = {};
obj.sub ??= {};
Open your app by chrome 79 or safari 13.0. The full error message looks like this:
SyntaxError: Unexpected token '?' // 13.js:2
MAX RELOADS REACHED // chunk-load-handler.js:24
ChunkLoadError: Loading chunk 13 failed. // trackConsoleError.js:25
(missing: http://example.com/13.js)

How to Change Firefox Proxy Settings Programmatically?

I'm launching Firefox via command line and I'd like to launch a specific Firefox Profile with a proxy. According to this answer on Stackoverflow, Firefox proxy settings are stored in pref.js in the Firefox Profile folder and it is necessary to edit this file to launch FF with a proxy.
I've edited the file as follows:
user_pref("network.proxy.ftp", "1.0.0.1");
user_pref("network.proxy.ftp_port", 00000);
user_pref("network.proxy.gopher", "1.0.0.1");
user_pref("network.proxy.gopher_port", 00000);
user_pref("network.proxy.http", "1.0.0.1");
user_pref("network.proxy.http_port", 22222);
user_pref("network.proxy.no_proxies_on", "localhost, 1.0.0.1");
user_pref("network.proxy.socks", "1.0.0.1");
user_pref("network.proxy.socks_port", 00000);
user_pref("network.proxy.ssl", "1.0.0.1");
user_pref("network.proxy.ssl_port", 00000);
user_pref("network.proxy.type", 1);
Note: the IP address and port used above are for demonstration purposes.
However, I'm encountering two problems:
1) Firefox completely ignores these settings and launches FF without any proxy at all
2) When Firefox exits the text modification is reverted/deleted
Note: When I edited the text file above, Firefox was not running. I know there's a disclaimer at the top of prefs.js:
If you make changes to this file while the application is running, the
changes will be overwritten when the application exits.
But there were no live instances of Firefox running at the time I edited the above file.
Manually creating different FF Profiles (as suggested by another user) with different proxies is not an option as everything needs to be done programmatically, without manual intervention.
Does Firefox still support linking proxy via pref.js? If not, what is the current working solution to launch Firefox via command line with a proxy in Java?
Thanks
A proxy-autoconfig file is what you are looking for.
Docs here.
Define a file name.pac, that contains the javascript function
function FindProxyForURL(url, host)
Inside the file you can use any javscript you'd like to decide what proxy to use. Set the path to your .pac file in the firefox settings, under auto-config proxy. Remember to use a file url.
To setup automatic file switching, simply configure firefox to point towards a single file, and overwrite the file programmatically every time you want it to change. You could keep copies of all options, and simply copy an option file into the target file right before running.
An example of a super simple pac file is this:
function FindProxyForURL (url, host) {
return 'PROXY proxy.example.com:8080; DIRECT';
}
It will always return the identical proxy for all endpoints.
Passwords are not explicitly supported by the pac standard, but there are different ways to approach this. Firefox will prompt you for a login if it thinks it needs one, and you could also embed the password into the url (username:password#proxy.example.com). Additionally, a tool like proxy login automator could allow you to use passwords and to dynamically set the proxy without having to fight with firefox.

Electron - Why is there a big delay when loading the main window through localhost compared to an HTML file?

When loading my main window by passing the HTML file directly through Electron, everything works as expected.
Electron app:
mainWindow.loadURL(
url.format({
pathname: path.join(__dirname, "mainWindow.html"),
protocol: "file:",
slashes: true
})
);
But when using Express and accessing the main window through localhost, there is a big delay (white screen) at the first launch of the Electron app which lasts for about 30 seconds.
However, The page is accessible through localhost in the browser as soon as the I run electron .
Express app:
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "mainWindow.html"));
});
app.listen(3000);
Electron app:
mainWindow.loadURL("http://localhost:3000");
I've tried this with multiple ports and they all give the same result.
Browsers can sometimes try to be a bit more clever than they need too, proxy server's used to be common in the early days of the web. And in co-operate companies they are still popular. But in most case's proxy servers can be handled transparently by the OS, so why browsers still try to handle this I'm not 100% sure.
But the easy fix is to tell the embedded Chromium not to try a resolve the proxy server.
app.commandLine.appendSwitch('auto-detect', 'false');
app.commandLine.appendSwitch('no-proxy-server')
You may not need both the above switches.
Of course if you app is running on a system that does use a proxy, it might have issues. But I've a feeling even then it's unlikely to cause an isssue, as hopefully the OS would be handling this anyway.
Also this might be handy keeping an eye on -> https://github.com/electron/electron/issues/13829

Reload javascript and css files for a website in Safari

This drives me crazy. Every browser and device has it's own way to clear the cache and reload the .js and .css files. For example, the only way I can get do this in Chrome is to go to go to my login page, do an F12 to load the debugger and proceed. This seems to be the only way to do it windows.
On the old safari in windows you could do it from the menu, as you seem to be able to do in Safari on the IMac desktop.
But how do you do in on the IPhone or IPad??????
I am using the IMac to debug the phone, but no matter how I do it, the .js file stays the same. When I look on the website at the page, the changes are there.
I have tried crashing the phone, double tapping the refresh button, putting it in private mode and back. Nothing works. What was interesting was that I tried it in private move and in the Develop menu I could see two versions of the .js file - one with the changes and one without. When I went back to the non-private mode, the old .js files (no changes) were there.
I also tried to delete the website data for my site and that didn't work.
Anyone know how to do it??????
Thanks,
Tom
Append a query string when including your JS or CSS and change it when you update the code. It will invalidate the cached local versions of this file.
<script src="test.js?1234567"></script>
It's common to use a timestamp.
2 answers:
First Answer: Hit the tabs icon, then select Private and paste&go the URL if you only need to force refresh the one time. The Private session will never have a cached version of anything unless it is cached at an intervening proxy. Of course it will cache subsequent requests for the same file. You can close the Private session and create a new one, which is roughly on par with the annoyance level of switching to/from the Settings app.
Second Answer: If you are a developer and you're using Apache, you can set up an .htaccess file like so:
<filesMatch "\.(html|htm|js|css)$">
FileETag None
<ifModule mod_headers.c>
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Wed, 21 Oct 2015 07:28:00 GMT"
</ifModule>
</filesMatch>
I've found this solution works for iOS Safari and other browsers ...
It performs a window.location.reload(true) to force the page to reload avoiding use of caching whenever it detects that the cached web app or web page does not match the server's version.
You really want this mechanism in place before you publish the first version of your site as otherwise once it's out there you can no longer rely on ever upgrading your iOS users. This is a nightmare if you rely on client app and server versions being in sync. Sigh ... Sadly it seems Safari and iOS Safari in particular is eager to take the throne once held by Internet Explorer.
During deployment
Increment and save a build number.
I use a version.json file that looks like {"major":1,"minor":0,"patch":0,"build":12}
Copy version.json to both client and server apps.
On server:
Write a server API in PHP/Node/Ruby/C# that returns the server's version.json
Use web server rules and response headers to prevent API results being cached.
In client app or web page:
To diagnose issues, display the client version + client and server build numbers.
Call on page load ...
function onload() {
var clientVersion = require("./version.json");
// Might replace axios with new fetch() API or older XMLHttpRequest
axios.get("/api/version").then(function(res) {
var serverVersion = res.data;
var lastReload = parseInt(localStorage.getItem("lastReload") || "0");
var now = new Date().getTime();
if (
serverVersion.build !== clientVersion.build &&
now - lastReload > 60 * 1000 // Prevent infinite reloading.
) {
localStorage.setItem("lastReload", now);
window.location.reload(true); // force page reload
}
});
}
You might prefer to test serverVersion.build > clientVersion.build rather than serverVersion.build !== clientVersion.build. The benefit of testing !== is that it allows you not only upgrade the version but also roll back a version and ensure clients get rolled back as well.
In the event that the client and server build numbers are mismatched I prevent the client from infinitely reloading by only performing a single reload within 60 secs.
Note that this method is only concerned with matching build numbers (= deployment number) you might prefer something more sophisticated.
Here's a simple node app to increment a build number.
const APP_DATA = "../app/src/assets"; // client folder
const FUNC_DATA = "../functions/data"; // server folder
var log = require("debug")("build:incr");
var fs = require("fs");
function readJson(path) {
log(`Reading: ${path}`);
const text = fs.readFileSync(path);
return JSON.parse(text);
}
function writeJson(path, json) {
log(`Writing: ${path}`);
fs.writeFileSync(path, JSON.stringify(json));
}
let version = readJson("./version.json");
version.build++;
log("Version = ", version);
writeJson("./version.json", version);
writeJson(`${APP_DATA}/version.json`, version);
writeJson(`${FUNC_DATA}/version.json`, version);
In iOS 10, you can clear the cache by going to Settings > Safari and clicking the "Clear History and Website Data".
For me, this is a bit overkill as I don't want to delete everything for every site. So, what I do is browse directly to the js or css file and reload it if it is the older, cached version.
I don't think there is a "easy" way to do this. I've tried all of the above including modifying css and js query strings. The closest thing that works is in Rockadile's comment.
Open Setting--Safari--Website Data
Then clear the site you are trying to refresh
Tip: If you are debugging a page use the IP Address then it will show up at the top of this list. Keep both safari and the Website data open so you can toggle between the two.
This isn't as simple as ctl-F5 but is certainly less intrusive than clearing the entire browser's cache.
Goto Safari -> Preferences -> click the Advanced tab at the bottom of the Advanced tab click the checkbox that says "Show Develop menu in bar" and close Preferences. Then you can open the Develop menu in the program bar that has Safari, File, Edit, View, etc... in it, and click Empty Caches.

Categories