TypeError: Failed to fetch, 400 error in network console, but not in exception

I am attempting to call the Google Drive REST API to upload a file from a browser context using the fetch api, but am not able to progress past a weird error.

TypeError: Failed to fetch

Looking at other posts on the topic, some point to a CORS failure, but it’s pretty difficult to determine exactly what the error is from this message. The strange thing is, looking at the Network tab of the Chrome devtools tells a different story:

enter image description here

Now I’ll be the first to admit that there’s a (high?) chance I’m sending a malformed request, but it’s also pretty difficult to determine what about the request is malformed without any error message.

What I’m doubly confused by, is why the fetch call is rejecting (throws error with await), rather than resolving with a 400 response.

The code to make the call is about as basic as it gets:

await fetch(`https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart`, options);

The options object is pretty boiler-plate, and just sets things like an Authorization header, Content-Type, Content-Length, as well as the body which (in this case) is multipart/related

So I neither get a graceful resolve response with a message, nor do I get useful reject outcome, rather just TypeError: Failed to fetch

If I were to rely on the rejection alone, I would have no idea this was actually a 400. It was only the Network tools that revealed this.

Anyone have a clue why this 400 error would be “swallowed”, and what I need to do to capture it?

Additional Edits

Further detail in response to comments. The code around the fetch looks like this:

async send() {
  this._build();
  return await fetch(this._parameterizedUrl, this._options);
}

This is contained in an object which just encapsulates the process of creating a request (called a Request). So the this pointer refers to the encapsulating object. this._parameterizedUrl is just the value of the URL string already posted, and this._options is the options object already mentioned. The call to build() just assembles the values of the Request object, mostly into the options argument (but also supports URL parameters, not used in this case)

The code calling this looks like:

const request = Request.post(uploadUrl);
request.setContentType(contentType);
request.setBody(body);
request.setAccessToken(accessToken);
request.addHeader('Content-Length', contentLength);
const response = await request.send();

The Request.post(uploadUrl) call is a shorthand to create my Request object (this is my own encapsulation as mentioned)

This call will throw an error, which is caught at an earlier point in the stack. The next line of the trace from the Failed to fetch error looks like this:

TypeError: Failed to fetch
    at Request.send (api.js:240:1)

Full Reproduction

document.addEventListener('DOMContentLoaded', async () => {
  
  const url = 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart';
  const options = {};
  const authToken = '<Add Auth Token>';
  const boundary = 'boundary-' + Math.random().toString(36).substring(2);
  const contentType = `multipart/related; boundary=${boundary}`;

  // Body part meta data for the filename
  const meta = {
    name: 'test.json'
  };

  // Body part for media (the file)
  const data = {
    foo: "bar",
    bar: "baz"
  }

  const metaBlob = new Blob([JSON.stringify(meta)], {
    type: 'application/json',
  });

  const mediaBlob = new Blob([JSON.stringify(data)], {
    type: 'application/json',
  });

  const body = new FormData();
  body.append('metadata', metaBlob, "metadata");
  body.append('media', mediaBlob, name);
  let contentLength = metaBlob.size + mediaBlob.size;

  const headers = new Headers();
  headers.append('Authorization', `Bearer ${authToken}`);
  headers.append('Content-Type', contentType);
  headers.append('Content-Length', contentLength);

  options['method'] = 'POST';
  options['headers'] = headers;
  options['body'] = body;

  try {
    const response = await fetch(url, options);
    console.log(response);
  } catch (err) {
    console.error(err);
  }
});