How to really know if your webapp is online
If you search for "check online JavaScript", you'll quickly come across the navigator.onLine
property.
Unsurprisingly, it returns true
if you're online, and false
if you aren't. In addition, the
window.ononline
and
window.onoffline
events notify you whenever that value changes.
False positives
If it was quite this easy, I wouldn't be sitting here writing a blog post about it. In fact, if we look at the spec, we find the following:
The onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail), and must return true otherwise.
In other words, navigator.onLine
being true does not necessarily mean that there is a working internet connection.
Most implementations seem to only be checking if there's a connected network adapter. There are a number of
circumstances where that's the case, but the internet is still unreachable. For example:
- The user is only connected to an internal network
- The user is inside a virtual machine, and the virtual network adapter is connected, but the host system is offline
- The user uses a VPN which has installed a virtual network adapter that is always connected
Just try it
How do you actually know if there is a working internet connection? Well, there's really only one way. You have to try it out. Do a fetch request to an appropriate target and see whether or not you get a network error. A simple example is below:
async function isInternetConnectionWorking() {
if (!navigator.onLine) {
return false;
}
const headers = new Headers();
headers.append('cache-control', 'no-cache');
headers.append('pragma', 'no-cache');
try {
await fetch(window.location.origin, { method: 'HEAD', headers });
return true;
} catch (error) {
if (error instanceof TypeError) {
return false;
}
throw error;
}
}
A few things to note:
- First, we check
navigator.onLine
- if it's false, we are definitely offline according to the spec. If it's true, we run our test. - We set the
cache-control
andpragma
headers to prevent the browser from sending a cached response. - We request our own site's origin, that way there's no need to worry about cross-origin access rules.
- We make a
HEAD
request instead ofGET
. AGET
request would download the response content, which is unnecessary in this case. - If we get an error that's not a
TypeError
, we re-throw it. There are two possible reasons for that:- The arguments passed to fetch are illegal (should never happen with this code)
- The fetch was aborted (to prevent that, don't abort the fetch)
Actually, don't even try
While the above code works, it has a subtle problem: Potentially every time the function
isInternetConnectionWorking()
is called, it sends a new, non-cached HEAD
request.
That might put an undue burden on the network.
A typical web app already makes plenty of requests during normal use. We can monitor those to detect whether or not we're online. Only if no request has occurred recently, we send one like in the above example to check if we're still connected.
The revised example below uses a service worker to intercept network requests and update the online status. It also adds a
special route, /is-online
, that we can fetch to get the current online status. Note that service workers only work
in modern browsers, and only on secure pages (served via
HTTPS).
If you're still not using HTTPS, well, you should stop reading this blog and
get on it.
index.js
navigator.serviceWorker.register('/sw.js');
async function isInternetConnectionWorking() {
if (!navigator.onLine) {
return false;
}
const response = await fetch('/is-online');
if (response.status === 200) {
const onlineStatus = response.json();
return onlineStatus.value;
}
return true;
}
sw.js
// Interval in ms before we re-check the connectivity - change to your liking
const ONLINE_TIMEOUT = 10000;
let onlineStatus = { value: true, timestamp: new Date().getTime() };
async function getFromCache(request) {
// get from cache, if you have one...
throw new TypeError();
}
async function tryGetFromNetwork(request) {
const timestamp = new Date().getTime();
try {
const response = await fetch(request);
onlineStatus = { value: true, timestamp };
return response;
} catch (error) {
if (error instanceof TypeError) {
onlineStatus = { value: false, timestamp };
}
throw error;
}
}
async function getOnlineState() {
const now = new Date().getTime();
const headers = new Headers();
headers.append('cache-control', 'no-store');
// If the last online status is recent, return it
if (now - onlineStatus.timestamp < ONLINE_TIMEOUT) {
return new Response(
JSON.stringify(onlineStatus),
{ status: 200, statusText: 'OK', headers }
);
}
// Otherwise, attempt a real fetch to re-check the connection
else {
try {
await fetch(location.origin, { method: 'HEAD', headers });
onlineStatus = { value: true, timestamp: now };
} catch (error) {
if (error instanceof TypeError) {
onlineStatus = { value: false, timestamp: now };
} else {
throw error;
}
}
}
// Recursive call, this time the new status will be returned
return await getOnlineState();
}
self.addEventListener('fetch', event => {
if (event.request.method === 'GET' && event.request.url === `${location.origin}/is-online`) {
event.respondWith(getOnlineState());
}
else {
event.respondWith(
tryGetFromNetwork(event.request)
// If the fetch fails, get a response from cache if you have one
// If not, just delete the next line
.catch(() => getFromCache(event.request))
);
}
});
That's it! Now, every time your app makes a request, the service worker will note whether it succeeded or
not. From your app, you can fetch /is-online
to get the latest connection status. If it's older than
ONLINE_TIMEOUT
milliseconds, the service worker will re-check for you.