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?
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?
Share Improve this question edited Oct 27, 2017 at 23:30 Dan Fabulich asked Oct 27, 2017 at 19:56 Dan FabulichDan Fabulich 39.6k42 gold badges145 silver badges184 bronze badges 2- Your service worker is properly installed and registered? – Hunter Commented Oct 27, 2017 at 20:04
- It must be registered or it wouldn't be blowing up the page on refresh! Also, it shows up in the Dev Tools Applications tab as running/registered. – Dan Fabulich Commented Oct 27, 2017 at 20:07
4 Answers
Reset to default 15Based 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.
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;
});
})
);
});
Problem
I came across this problem when trying to override fetch
for all kinds of different assets. navigate
mode was set for the initial Request
that gets the index.html
(or other html
) file; and I wanted the same caching rules applied to it as I wanted to several other static assets.
Here are the two things I wanted to be able to accomplish:
- When fetching static assets, I want to sometimes be able to override the
url
, meaning I want something like:fetch(new Request(newUrl))
- At the same time, I want them to be fetched just as the sender intended; meaning I want to set second argument of
fetch
(i.e. theRequestInit
object mentioned in the error message) to theoriginalRequest
itself, like so:fetch(new Request(newUrl), originalRequest)
However the second part is not possible for requests in navigate
mode (i.e. the initial html
file); at the same time it is not needed, as explained by others, since it will already keep it's cookies, credentials etc.
Solution
Here is my work-around: a versatile fetch
that...
- can override the URL
- can override
RequestInit
config object - works with both,
navigate
as well as any other requests
function fetchOverride(originalRequest, newUrl) {
const fetchArgs = [new Request(newUrl)];
if (request.mode !== 'navigate') {
// customize the request only if NOT in navigate mode
// (since in "navigate" that is not allowed)
fetchArgs.push(request);
}
return fetch(...fetchArgs);
}
In my case I was contructing a request from a serialized form in a service worker (to handle failed POSTs). In the original request it had the mode
attribute set, which is readonly, so before one reconstructs the request, delete the mode
attribute:
delete serializedRequest["mode"];
request = new Request(serializedRequest.url, serializedRequest);