Request formData in Service Worker - TypeError: Failed to fetch

Viewed 808

I'm using service worker as a proxy, and it throws TypeError: Failed to fetch randomly and in a strange way.

Sometimes, const formData = await request.formData(); fails and throws TypeError: Failed to fetch.

I store the file in FormData and await request.formData() work just fine for most of the cases. But, if it fails for specific file one time, it will fail for all the next times, only for this specific file. The rest files would work(except this rare case occurs for them).

I would appreciate any idea what could be possibly wrong, is it a Service worker bug, or maybe there a specific set of conditions that could potentially cause this behaviour.

async function handleUploadFileRequest(request) {
  const formData = await request.formData();
  const file = formData.get('file');

  const response = await fetch(`${endpoint}/api/v2/uploads.json?filename=${file.name}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/binary'
    },
    body: file,
  });

  return response.clone();
}

self.addEventListener('fetch', (event) => {
  if (!token || !endpoint) {
    const params = new URL(location).searchParams;
    endpoint = params.get('endpoint');
    token = params.get('token');
  }

  const handleRequest = async () => {
    try {
      const { request } = event;

      if (request.method === 'POST') {
        const isUploadFileRequest = request.url.includes('/request_uploads');
        const isSubmitFormRequest = request.url.includes('/request');

        if (isUploadFileRequest) {
          return handleUploadFileRequest(request);
        }

        if (isSubmitFormRequest) {
          return handleSubmitRequest(request);
        }
      }

      if (request.method === 'DELETE') {
        const isDeleteFileRequest = request.url.includes('/requests/new');

        if (isDeleteFileRequest) {
          return new Response(true, { status: 200 });
        }
      }

      if (request.method === 'GET') {
        const isSuggestionsRequest = request.url.includes('/suggestions');

        if (isSuggestionsRequest) {
          const url = request.url;
          const newUrl = `${url}&jwt=${token}`;
          const newRequest = new Request(newUrl, request);
          const response = await fetch(newRequest);

          return new Response(await response.text(), {
            status: response.status,
            headers: { 'content-type': 'application/json' }
          });
        }

        const isAnswerBotRequest = request.url.includes('/answerBot.js');

        if (isAnswerBotRequest) {
          return new Response('var answerBot = { init: (locale) => {}};', { status: 200 });
        }
      }

      return fetch(request);
    } catch (error) {
      return fetch(request);
    }
  };

  event.respondWith(handleRequest());
});

1 Answers

You can only "consume" the body of a Request object once.

Inside of handleUploadFileRequest(), you end up consuming the body when you call request.formData(). If handleUploadFileRequest() ends up throwing an exception for any reason, your

catch (error) {
  return fetch(request);
}

block is triggered, at which point you end up attempting to consume request's body for a second time. That's always going to fail.

I'd suggest two changes:

  1. Clone the request object before consuming its body:
function handleUploadFileRequest(request) {
  const clonedRequest = request.clone();
  const formData = await clonedRequest.formData();
  // ...
}

By doing this, you'll leave the original Request object's body undisturbed. You need to call clone() for any codepath that might end up consuming the request body if you want your fetch(request) in the catch() block to work.

  1. Add in some logging to your catch() handler to figure out why the exception is being thrown:
catch (error) {
  console.log('Falling back to network due to: ', error);
  return fetch(request);
}

I'm not sure what's currently causing your failure, but hopefully some extra logging will help you detect what's going wrong, and the clone() should allow your fallback to actually succeed.

Related