14

I keep getting this error:

Uncaught (in promise) DOMException: Failed to execute 'respondWith' on 'FetchEvent': The event has already been responded to.

I know that service workers automatically respond if asynchronous stuff goes on within the fetch function, but I can't quite work out which bit would be the offender in this code:

importScripts('cache-polyfill.js');

self.addEventListener('fetch', function(event) {

  var location = self.location;

  console.log("loc", location)

  self.clients.matchAll({includeUncontrolled: true}).then(clients => {
    for (const client of clients) {
      const clientUrl = new URL(client.url);
      console.log("SO", clientUrl);
      if(clientUrl.searchParams.get("url") != undefined && clientUrl.searchParams.get("url") != '') {
        location = client.url;
      }
    }

  console.log("loc2", location)

  var url = new URL(location).searchParams.get('url').toString();

  console.log(event.request.hostname);
  var toRequest = event.request.url;
  console.log("Req:", toRequest);

  var parser2 = new URL(location);
  var parser3 = new URL(url);

  var parser = new URL(toRequest);

  console.log("if",parser.host,parser2.host,parser.host === parser2.host);
  if(parser.host === parser2.host) {
    toRequest = toRequest.replace('https://booligoosh.github.io',parser3.protocol + '//' +  parser3.host);
    console.log("ifdone",toRequest);
  }

  console.log("toRequest:",toRequest);

  event.respondWith(httpGet('https://cors-anywhere.herokuapp.com/' + toRequest));
  });
});

function httpGet(theUrl) {
    /*var xmlHttp = new XMLHttpRequest();
    xmlHttp.open( "GET", theUrl, false ); // false for synchronous request
    xmlHttp.send( null );
    return xmlHttp.responseText;*/
    return(fetch(theUrl));
}

Any help would be appreciated.

Ethan
  • 3,410
  • 1
  • 28
  • 49

2 Answers2

22

The problem is that your call to event.respondWith() is inside your top-level promise's .then() clause, meaning that it will be asynchronously executed after the top-level promise resolves. In order to get the behavior you're expecting, event.respondWith() needs to execute synchronously as part of the fetch event handler's execution.

The logic inside of your promise is a bit hard to follow, so I'm not exactly sure what you're trying to accomplish, but in general you can follow this pattern:

self.addEventListerner('fetch', event => {
  // Perform any synchronous checks to see whether you want to respond.
  // E.g., check the value of event.request.url.
  if (event.request.url.includes('something')) {
    const promiseChain = doSomethingAsync()
      .then(() => doSomethingAsyncThatReturnsAURL())
      .then(someUrl => fetch(someUrl));
      // Instead of fetch(), you could have called caches.match(),
      // or anything else that returns a promise for a Response.

    // Synchronously call event.respondWith(), passing in the
    // async promise chain.
    event.respondWith(promiseChain);
  }
});

That's the general idea. (The code looks even cleaner if you end up replacing promises with async/await.)

Jeff Posnick
  • 53,580
  • 14
  • 141
  • 167
15

I also stumbled upon this error while trying to use async/await within a fetch handler. As Jeff mentioned in his answer, event.respondWith has to be called synchronously and the parameter can be anything that returns a promise that resolves to a response. Since async functions do return a promise, all you have to do is wrap the fetch logic inside an async function which, at some point, returns a response object and call event.respondWith with that handler.

async function handleRequest(request) {
  const response = await fetch(request)

  // ...perform additional logic

  return response
}

self.addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request));
});
Pier-Luc Gendreau
  • 13,553
  • 4
  • 58
  • 69