Why do you need to detect redirects in a PWA page? Well a PWA is established on a specific domain. You might need to redirect to a new domain at some point. You can easily do it server side with simple commands, but as PWA is cached in a specific way it will remain on the old domain. PWA will not be able to do calls to redirected API because of CORS. And it might never move on because when your admin redirected domains you will not be able to get a new service worker either. So you need to think ahead and plan for this. But how? Can you detect redirects, especially CORS redirects in JavaScript?
You might think that detecting redirects is easy – just do a an XHR or fetch and check HTTP status code, right? Wrong, sorry. Not that easy. By default all redirects are resolved in both XHR and fetch. This is done in a very early stage. Between readyState = 1
(opened request) and readyState = 2
(headers received).
HEADERS_RECEIVED
(numeric value 2):
All redirects (if any) have been followed and all headers of a response have been received.")
Source: xhr.spec.whatwg.org/#states
This means that your browser will try to go destination site and will fail due to CORS. So in xhr.onreadystatechange
you will go straight from opened (1) to done (4). You will also get an error
event. For fetch
this is similar, but you will get a TypeError
with a message a bit depended on a browser (different in Chrome and Firefox).
This is not useful because you get the same error when CORS fails and when the server is not working. And with PWA your app should still be usable when the server is offline. In either case status = 0
and headers are removed. Annoying, right? I agree, but this is due to security reasons.
Exposing redirects might leak information not otherwise available through a cross-site scripting attack.
Example:
A fetch tohttps://example.org/auth
that includes a Cookie markedHttpOnly
could result in a redirect tohttps://other-origin.invalid/4af955781ea1c84a3b11
. This new URL contains a secret. If we expose redirects that secret would be available through a cross-site scripting attack.
Source: fetch.spec.whatwg.org/#atomic-http-redirect-handling
So that is a no-go for detecting CORS redirects. It also means there will probably never be a way to detect the destination URL of a redirect.
At this point we know XHR is no use for us. So how about a newer Fetch API? Above still mostly applies. Response status will be 0 and headers are not available. But there is a special fetch request that doesn't resolve redirects.
So to detect a redirect you need to:
fetch()
request.method: 'HEAD'
will be good for that.cache: 'no-cache'
option.redirect: 'manual'
option.response.type
. It will be opaqueredirect
when a response indicated a redirect.Note that for this request headers will still not be available. For the same security reasons. So you will not be able to get a Location
header, but at least you know the redirect occurred. I don't agree with the specs here. It should be website dependent. Not all websites redirect with a session ID in the URL... But hey, you can shout it out on Twitter or Tiktok to some dev evangelist 🙂.
Specification for redirect
fetch option and opaqueredirect
response type:
Note that this will brake JavaScript in the Internet Explorer. So if you care about IE users getting some JS, then load this in a separate <script>
tag.
This is also not a final code I used, but it is close. It has some extra logging so you can see what is going on. Remember to at least change url
to something specific to your website.
/** Redirect detection testing. (not a final impl, just a good code for browser behaviour testing) */ async function isRedirect(url) { // request that is designed to be as light as possible const response = await fetch(url, { // this is crucial redirect: 'manual', /** Use HEAD method for a light request and response. This should also skip SW caches (you typically only cache GET requests in SW) */ method: 'HEAD', // make sure HTTP cache is skipped cache: 'no-cache', // below potentially makes the request light, but might change server behaviour... // YMMV mode: 'no-cors', credentials: 'omit', referrerPolicy: 'no-referrer', }); console.log('[isRedirect]', response); console.log('[isRedirect]', 'Crucial state:', { status: response.status, type: response.type, url: response.url, }); if (response.type == 'opaqueredirect') { return true; } return false; } var url = '/templates/templates.html'; // Note that you have to catch network errors here (you cannot use `await`). var result = false; isRedirect(url).then((isRedirectResult) => { result = isRedirectResult; console.log('request ok, redirect:', isRedirectResult); console.log('result:', result, url); }).catch((error) => { console.error('error caught:', error); console.log('result:', result, url); });
As a side note: I used var
variables in global scope intentionally. If you haven't noticed already, note that you cannot replay (re-run) code that uses let
variables.
So you might use above as a base for your own class or just use this quick snippet:
fetch(urlForTestingRedirs, { redirect: 'manual', method: 'HEAD', cache: 'no-cache', }).then(function(response) { if (response.type == 'opaqueredirect') { clearAllForPwa() } });
You do have to define clearAllForPwa
though. The function will be specific to your application, but:
navigator.serviceWorker.getRegistrations()
should work fine. But just make sure getRegistrations
is a function (or try-catch over that).clearServiceWorkerCache
function).localStorage.clear()
.localforage.clear()
. Note that after running this (even if you wait) a IDB database will not be removed until you leave the page. That is expected and don't worry about that too much.So next time your user will do a navigation the redirect will just work. You might also pop-up some message about errors with a reload button. You should probably wait until all caches are clear though.
So that's it. Not that hard once you know you have to use that one specific option of fetch()
(and that you cannot use headers nor status).