Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google Storage API - Resumable Upload, AJAX, CORS Error

Getting a strange error.

When a user wants to upload a file, they send an AJAX request to my server. My server authenticates server-side with OAuth2 to Google's servers, creates an access token, begins a resumable upload, and passes the resumable upload URI and the access token to the browser.

Then, the browser uploads directly to Google Storage.

Everything seems to be fine. The file gets to the Storage Bucket no problem, but I am still receiving a CORS error on Chrome and I'm not sure where or why. Here is a simplified version of my javascript code:

var file = document.getElementById("fileInput").files[0];

var request = requestUpload();
var token = request.token;
var uri = request.uri;

var r = new XMLHttpRequest();
r.open('PUT', uri, true);
r.setRequestHeader("Authorization", "Bearer "+token);
r.send(file);`

Pretty simple- but I'm still getting this common error:

XMLHttpRequest cannot load https://www.googleapis.com/upload/storage/v1/b/*****. No 'Access-Control-Allow-Origin' header is present on the requested resource.

Despite the fact that it seems to be completely functional. Is Javascript requesting to see something I'm not allowed to by some default? I'd like not to have errors.

The XMLHttpRequest object also triggers an error event after everything is finished uploading. I'm guessing the request is expecting some kind of feedback from Google that it isn't getting and JavaScript is becoming the Latin of JavaScript so most of the discussion I've found is regarding jQuery.

Many thanks!

like image 694
c.. Avatar asked Dec 03 '14 21:12

c..


2 Answers

I just ran into this exact issue. Had me scratching my head for a little bit, but I figured out a solution. It is pretty poorly documented, and the behavior doesn't exactly make sense, but if you look at the last bullet point on this page

https://cloud.google.com/storage/docs/cross-origin

When using the resumable upload protocol, the Origin from the first (start upload) request is always used to decide the Access-Control-Allow-Origin header in the response, even if you use a different Origin for subsequent request. Therefore, you should either use the same origin for the first and subsequent requests, or if the first request has a different origin than subsequent requests, use the XML API with the CORS configuration set to *.

So what I did was I added the Origin: http://example.com request header on the resumable session initiation request from the server. That way all the client requests that correspond to that upload session id will check against that origin.

The one thing that I still find strange is the OPTIONS preflight request made by the browser (at least for me) was allowing anything through even though the PUT request was failing.

like image 124
Craig Avatar answered Oct 13 '22 01:10

Craig


Even with this configuration when executing gsutil cors get gs://your-bucket, you will need to initiate the initial POST request according to @Craig 's answer:

[{"maxAgeSeconds": 3600, "method": ["GET", "PUT", "POST", "HEAD", "DELETE"], "origin": ["*"], "responseHeader": ["*"]}].

Using for example golang it is easy to pass the Origin on to the gcloud request. Having a r http.Request input, you could execute r.Header.Get("Origin") and pass it on to a function calling the POST on gcloud. This is a function i wrote doing this in golang:

func GetUploadURL(bucket, object, contentType, origin string, expires time.Time) (string, error) {
    url, err := storage.SignedURL(bucket, object, &storage.SignedURLOptions{
        GoogleAccessID: "[email protected]",
        PrivateKey:     pkey, // set this in package init() function
        Method:         http.MethodPost,
        Expires:        expires,
        ContentType:    contentType,
        Headers:        []string{"x-goog-resumable:start"},
    })

    if err != nil {
        return "", err
    }

    req, err := http.NewRequest("POST", url, nil)
    if err != nil {
        return "", err
    }
    req.Header.Set("Content-Type", contentType)
    req.Header.Set("x-goog-resumable", "start")
    req.Header.Set("Origin", origin)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    return resp.Header.Get("Location"), nil
}
like image 31
Franz Von Der Lippe Avatar answered Oct 12 '22 23:10

Franz Von Der Lippe