最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Can a service worker fetch and cache cross-origin assets? - Stack Overflow

programmeradmin0浏览0评论

I'm using some service worker code from the Progressive Web app tutorial by Google but I am getting an error:

Uncaught (in promise) TypeError:
 Failed to execute 'clone' on 'Response':
 Response body is already used

The site uses third-party Javascript and stylesheets for web fonts. I want to add assets hosted on these CDNs to the offline cache.

addEventListener("fetch", function(e) {
  e.respondWith(
    caches.match(e.request).then(function(response) {
        return response || fetch(e.request).then(function(response) {
        var hosts = [
          "",
          "",
          ""
        ];
        hosts.map(function(host) {
          if (e.request.url.indexOf(host) === 0) {
            caches.open(CACHE_NAME).then(function(cache) {
              cache.put(e.request, response.clone());
            });
          }
        });
        return response;
      });
    })
  );
});

These are hosted on popular CDNs, so my hunch is they should be doing the right thing for CORS headers.

Here are the assets in the HTML that I want to cache:

<link rel="stylesheet" type="text/css"
      href=":900,900italic,300,300italic">
<link rel="stylesheet" type="text/css"
      href=":900,300" rel="stylesheet">
<link rel="stylesheet" type="text/css"
      href=".min.css">
<script type="text/javascript" async
        src=".7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>

According to the console logs, the service worker is trying to fetch these assets:

Fetch finished loading:
 GET ".min.css".
 sw.js:32
Fetch finished loading:
 GET ".7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML".
 sw.js:32
Fetch finished loading:
 GET ":900,900italic,300,300italic".
 sw.js:32
Fetch finished loading:
 GET ":900,300".
 sw.js:32
Fetch finished loading:
 GET ".7.1/config/TeX-AMS-MML_HTMLorMML.js?V=2.7.1".
 sw.js:32
Fetch finished loading:
 GET ".woff2?v=4.7.0".
 sw.js:32

If I remove the clone, as was suggested in Why does fetch request have to be cloned in service worker?, I'll get the same error:

TypeError: Response body is already used

If I add { mode: "no-cors" } to the fetch per Service worker CORS issue, I'll get the same error and these warnings:

The FetchEvent for
 ".woff2?v=4.7.0"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors
The FetchEvent for
 ".woff2"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors
The FetchEvent for
 ".woff2"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors
The FetchEvent for
 ".woff2"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors

I could add these assets to the static cache in the service worker's install event, but I have reasons to add them to the cache only in the fetch event.

I'm using some service worker code from the Progressive Web app tutorial by Google but I am getting an error:

Uncaught (in promise) TypeError:
 Failed to execute 'clone' on 'Response':
 Response body is already used

The site uses third-party Javascript and stylesheets for web fonts. I want to add assets hosted on these CDNs to the offline cache.

addEventListener("fetch", function(e) {
  e.respondWith(
    caches.match(e.request).then(function(response) {
        return response || fetch(e.request).then(function(response) {
        var hosts = [
          "https://fonts.googleapis.com",
          "https://maxcdn.bootstrapcdn.com",
          "https://cdnjs.cloudflare.com"
        ];
        hosts.map(function(host) {
          if (e.request.url.indexOf(host) === 0) {
            caches.open(CACHE_NAME).then(function(cache) {
              cache.put(e.request, response.clone());
            });
          }
        });
        return response;
      });
    })
  );
});

These are hosted on popular CDNs, so my hunch is they should be doing the right thing for CORS headers.

Here are the assets in the HTML that I want to cache:

<link rel="stylesheet" type="text/css"
      href="https://fonts.googleapis.com/css?family=Merriweather:900,900italic,300,300italic">
<link rel="stylesheet" type="text/css"
      href="https://fonts.googleapis.com/css?family=Lato:900,300" rel="stylesheet">
<link rel="stylesheet" type="text/css"
      href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css">
<script type="text/javascript" async
        src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
</script>

According to the console logs, the service worker is trying to fetch these assets:

Fetch finished loading:
 GET "https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css".
 sw.js:32
Fetch finished loading:
 GET "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML".
 sw.js:32
Fetch finished loading:
 GET "https://fonts.googleapis.com/css?family=Merriweather:900,900italic,300,300italic".
 sw.js:32
Fetch finished loading:
 GET "https://fonts.googleapis.com/css?family=Lato:900,300".
 sw.js:32
Fetch finished loading:
 GET "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/config/TeX-AMS-MML_HTMLorMML.js?V=2.7.1".
 sw.js:32
Fetch finished loading:
 GET "https://maxcdn.bootstrapcdn.com/font-awesome/latest/fonts/fontawesome-webfont.woff2?v=4.7.0".
 sw.js:32

If I remove the clone, as was suggested in Why does fetch request have to be cloned in service worker?, I'll get the same error:

TypeError: Response body is already used

If I add { mode: "no-cors" } to the fetch per Service worker CORS issue, I'll get the same error and these warnings:

The FetchEvent for
 "https://maxcdn.bootstrapcdn.com/font-awesome/latest/fonts/fontawesome-webfont.woff2?v=4.7.0"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors
The FetchEvent for
 "https://fonts.gstatic.com/s/lato/v14/S6u9w4BMUTPHh50XSwiPGQ3q5d0.woff2"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors
The FetchEvent for
 "https://fonts.gstatic.com/s/lato/v14/S6u9w4BMUTPHh7USSwiPGQ3q5d0.woff2"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors
The FetchEvent for
 "https://fonts.gstatic.com/s/merriweather/v19/u-4n0qyriQwlOrhSvowK_l521wRZWMf6hPvhPQ.woff2"
 resulted in a network error response: an "opaque" response was
 used for a request whose type is not no-cors

I could add these assets to the static cache in the service worker's install event, but I have reasons to add them to the cache only in the fetch event.

Share Improve this question asked Feb 10, 2019 at 18:31 ashawleyashawley 4,2831 gold badge28 silver badges42 bronze badges
Add a comment  | 

2 Answers 2

Reset to default 15

You're on the right track with using clone(), but the timing is important. You need to make sure that you call clone() before the final return response executes, because at that point, the response will be passed to the service worker's client page, and its body will be "consumed".

There are two ways of fixing this: either call clone() prior to executing the asynchronous caching code, or alternatively, delay your return response statement until after the caching has completed.

I'm going to suggest the first approach, since it means you'll end up getting the response to the page as soon as possible. I'm also going to suggest that you rewrite your code to use async/await, as it's much more readable (and supported by any browser that also supports service workers today).

addEventListener("fetch", function(e) {
  e.respondWith((async function() {
    const cachedResponse = await caches.match(e.request);
    if (cachedResponse) {
      return cachedResponse;
    }

    const networkResponse = await fetch(e.request);

    const hosts = [
      'https://fonts.googleapis.com',
      'https://maxcdn.bootstrapcdn.com',
      'https://cdnjs.cloudflare.com',
    ];

    if (hosts.some((host) => e.request.url.startsWith(host))) {
      // This clone() happens before `return networkResponse` 
      const clonedResponse = networkResponse.clone();

      e.waitUntil((async function() {
        const cache = await caches.open(CACHE_NAME);
        // This will be called after `return networkResponse`
        // so make sure you already have the clone!
        await cache.put(e.request, clonedResponse);
      })());
    }

    return networkResponse;
  })());
});

Note: The (async function() {})() syntax might look a little weird, but it's a shortcut to use async/await inside an immediately executing function that will return a promise. See http://2ality.com/2016/10/async-function-tips.html#immediately-invoked-async-function-expressions

For the original code, you need to clone the response before you do the asynchronous cache update:

        var clonedResponse = response.clone();
        caches.open(CACHE_NAME).then(function(cache) {
          cache.put(e.request, clonedResponse);
        });

The Service Worker primer by Google has example code showing the correct way. The code has a comment with an "important" note, but it's just emphasizing the clone, and not the issue you're having about when you clone:

        // IMPORTANT: Clone the response. A response is a stream
        // and because we want the browser to consume the response
        // as well as the cache consuming the response, we need
        // to clone it so we have two streams.
        var responseToCache = response.clone();

        caches.open(CACHE_NAME)
          .then(function(cache) {
            cache.put(event.request, responseToCache);
          });

        return response;

One important thing to take care is, by default, cross origin request would get opaque response—which you can not inspect the headers and body. You can check the status code, however, for opaque response, it is always 0, hence your service worker has no way to know whether a request was successful or whether it resulted in an error, this is pretty dangerous, if the cached response is a corrupted one and you take the CacheFirst strategy, then your PWA would be corrupted forever.

Check more details on google workbox's doc

发布评论

评论列表(0)

  1. 暂无评论