Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit

Consider this sample index.html file.

<!DOCTYPE html>
<html><head><title>test page</title>
<script>navigator.serviceWorker.register('sw.js');</script>
</head>
<body>
<p>test page</p>
</body>
</html>

Using this Service Worker, designed to load from the cache, then fallback to the network if necessary.

cacheFirst = (request) => {
    var mycache;
    return caches.open('mycache')
        .then(cache => {
            mycache = cache;
            cache.match(request);
        })
        .then(match => match || fetch(request, {credentials: 'include'}))
        .then(response => {
            mycache.put(request, response.clone());
            return response;
        })
}

addEventListener('fetch', event => event.respondWith(cacheFirst(event.request)));

This fails badly on Chrome 62. Refreshing the HTML fails to load in the browser at all, with a "This site can't be reached" error; I have to shift refresh to get out of this broken state. In the console, it says:

Uncaught (in promise) TypeError: Failed to execute 'fetch' on 'ServiceWorkerGlobalScope': Cannot construct a Request with a Request whose mode is 'navigate' and a non-empty RequestInit.

"construct a Request"?! I'm not constructing a request. I'm using the event's request, unmodified. What am I doing wrong here?

like image 640
Dan Fabulich Avatar asked Oct 27 '17 19:10

Dan Fabulich


2 Answers

Based on further research, it turns out that I am constructing a Request when I fetch(request, {credentials: 'include'})!

Whenever you pass an options object to fetch, that object is the RequestInit, and it creates a new Request object when you do that. And, uh, apparently you can't ask fetch() to create a new Request in navigate mode and a non-empty RequestInit for some reason.

In my case, the event's navigation Request already allowed credentials, so the fix is to convert fetch(request, {credentials: 'include'}) into fetch(request).

I was fooled into thinking I needed {credentials: 'include'} due to this Google documentation article.

When you use fetch, by default, requests won't contain credentials such as cookies. If you want credentials, instead call:

fetch(url, {
  credentials: 'include'
})

That's only true if you pass fetch a URL, as they do in the code sample. If you have a Request object on hand, as we normally do in a Service Worker, the Request knows whether it wants to use credentials or not, so fetch(request) will use credentials normally.

like image 150
Dan Fabulich Avatar answered Sep 23 '22 04:09

Dan Fabulich


https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker

var networkDataReceived = false;
// fetch fresh data
var networkUpdate = fetch('/data.json').then(function(response) {
  return response.json();
}).then(function(data) {
  networkDataReceived = true;
  updatePage(data);
});

// fetch cached data
caches.match('mycache').then(function(response) {
  if (!response) throw Error("No data");
  return response.json();
}).then(function(data) {
  // don't overwrite newer network data
  if (!networkDataReceived) {
    updatePage(data);
  }
}).catch(function() {
  // we didn't get cached data, the network is our last hope:
  return networkUpdate;
}).catch(showErrorMessage).then(console.log('error');

Best example of what you are trying to do, though you have to update your code accordingly. The web example is taken from under Cache then network.

for the service worker:
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('mycache').then(function(cache) {
      return fetch(event.request).then(function(response) {
        cache.put(event.request, response.clone());
        return response;
      });
    })
  );
});
like image 21
Hunter Avatar answered Sep 23 '22 04:09

Hunter