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:
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);
}
});
2
Answers
In your showing script, how about the following modification?
Modification points:
headers.append('Content-Type', contentType);
andheaders.append('Content-Length', contentLength);
are removed.const res = await response.json();
. By this, the file metadata of the uploaded file can be seen withconsole.log(res);
.const contentType = `multipart/related; boundary=${boundary}`;
. But, the request body is automatically created byFormData()
. At that time, the boundary and content type are also automatically created. But, your given content type overwrites the automatically created content type. By this, the request body cannot be correctly parsed on the server side. So, I guessed that such an error occurred.Modified script:
When this script is run, when your access token is valid and Drive API is enabled, the following result is obtained. The file is created in the root folder.
For posterity, the reason you’re not seeing any details and the reason this goes directly to your
catch
block is indeed a CORS error.The API does actually support CORS, at least for preflight and successful requests…
The problem is one you’ll see in many implementations where CORS response headers are not added for some or all unsuccessful requests. In this particular case, your incorrect
content-type
header handling results in a 400 Bad Request response which is missing theAccess-Control-Allow-Origin
header so your client-side code has no access andfetch()
fails with aTypeError
.I would not be surprised if there wasn’t another error present in your Console. Something like