3

I have a Google App Engine app that works fine in production when everything is running on one host, and mostly works when the web app is running on a separate host. All queries to/from the server (GET, POST, PUT, DELETE) are behaving as expected. This indicates to me that I have all CORS configured correctly throughout the system (I fought that battle several weeks ago and have it all worked out).

The only piece that I cannot make work is file uploads. I am using django, djangoappengine, django-cors-headers, and filetransfers, and the net result of everything is that I am unable to upload files when running from a remote server but everything else is working correctly. In the JavaScript console in Chrome I am seeing the following error:

XMLHttpRequest cannot load http://localhost:8080/_ah/upload/ahl...<truncated>.
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:9000' is therefore not allowed
access. The response had HTTP status code 405.

This is clearly a CORS error, so I know roughly what needs to happen. Except I can't figure out how to make the necessary change to my configuration to overcome this.

Here's my overall setup:

  • dev_appserver.py serving the API on port 8080
  • grunt serve serving the client app on port 9000
  • CORS settings:
    • development: CORS_ORIGIN_ALLOW_ALL = True
    • production: CORS_ORIGIN_WHITELIST = [ '(app.domain.com for my app)' ]

In production, I believe the fix will be to configure CORS on my bucket, but I'm not positive. However, I have no idea how to configure the local development server for this so that I can test the overall flow of data before I deploy.

Here's the JavaScript that's ultimately failing (the app is using AngularJS):

var form = angular.element('#media-form');
var data = new FormData(form);

// have the API return a URL using prepare_upload in
// filetransfers module to upload to:
var uploadActionUrl = https://api.domain.com/upload_url/';
$http.get(uploadActionUrl)
  .then(function(response) {
    // I get here with no problem
    $http.post(response.data.action, formData)
      .then(function(response) {
        console.log('got:', response);
      }, function(error) {
        console.log('upload error:', error); // <- this is where I end up
      });
  }, function(error) {
    console.log('get upload URL error:', error);
  });

Again, code very much like this functions correctly when run from the same host (so the API itself is functioning correctly), and (importantly) all HTTP methods work on all endpoints other than uploading my file, so CORS itself is set up correctly for interactions with App Engine. It's only the file upload piece that is not functioning.

It occurs to me that perhaps the fix includes assembling my form for upload using JSON instead of FormData, but I have never found a way to do this in the past.

--- UPDATED TO ADD ---

As a point of clarification, the endpoint which is causing this error is not inside my app directly, it is at a URL handled by a separate Google service. The code that gives me the URL is:

from google.appengine.ext.blobstore import create_upload_url

def prepare_upload(request, url, **kwargs):
    return create_upload_url(
        url,
        gs_bucket_name = settings.GOOGLE_CLOUD_STORAGE_BUCKET
    ), {}

The URL I get back is of the form /_ah/upload/<one-time key>, and everything that happens at that URL is (it seems) outside of my control, including adding headers.

seawolf
  • 2,147
  • 3
  • 20
  • 37
  • When I say 'this works when run from the same host', I should clarify. The details of the post and response handling are a little different as the previous app was not using AngularJS; it's possible that I am also doing other things slightly wrong here, but I can't troubleshoot any of that until I solve the CORS issue. – seawolf Mar 04 '16 at 22:59
  • Does the handler for `url` send CORS headers? Here is an example of what you need to do to make it work: https://github.com/GoogleCloudPlatform/appengine-python-blobstore-cors-upload/blob/master/main.py – Josh J Mar 16 '16 at 14:00
  • why not directly POST the file in formData to the endpoint instead of first get and then post? Like `var input = $('selector'); var data = new FormData(); data.append('name', input.name); data.append('type', input.type); data.append(input.name, input.files[0]); $.ajax({ url: 'url', type: 'POST', processData: false, contentType: false, // important for jqXHR data: data}.done()` – dliu Mar 17 '16 at 03:41
  • The endpoint URL is created on-the-fly by the data store and is only usable once, so I have to retrieve the URL from the server (the GET) and then POST to that endpoint with the file to be uploaded. – seawolf Mar 17 '16 at 18:30
  • @seawolf Did you ever find a solution for this? I've run into the same issue wherein my `POST` to `/_ah/upload` is failing during the preflight `OPTIONS` request. – redhotvengeance Dec 07 '16 at 06:13
  • 1
    I never did find a good solution. Instead, I created a form on the AppEngine project that has a couple JavaScript hooks to signal important events (loaded, file selected, upload complete, etc), and then I embed that form as an IFrame in the client app and listen for those events. It's ugly, but it gets the job done. – seawolf Dec 08 '16 at 16:59

2 Answers2

1

One way is to set http headers for the particular url in your app.yaml

So for example:

handlers:
- url: /_ah/upload....
  ...
  http_headers:
    Access-Control-Allow-Origin: http://localhost:9000
Jeffrey Godwyll
  • 3,787
  • 3
  • 26
  • 37
  • 3
    This particular setting is only possible on static file handlers (see [Static File Handlers](https://cloud.google.com/appengine/docs/python/config/appconfig)). I was very hopeful when I first found that a few days ago but it doesn't help in this case as far as I can tell. – seawolf Mar 04 '16 at 22:57
1

Your http handlers must have OPTIONS method for sending cors headers to browser.

For example; Chrome always sends an OPTIONS request to same URL before PUT requests for checking CORS headers. If browser can't get response for OPTIONS request, cors will fail.

Check this app engine webapp2 example.

class BaseRestHandler(webapp2.RequestHandler):
    def options(self, *args, **kwargs):
        self.response.headers['Access-Control-Allow-Origin'] = '*'
        self.response.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept'
        self.response.headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE'

https://en.wikipedia.org/wiki/Cross-origin_resource_sharing

Kenan Kocaerkek
  • 319
  • 3
  • 9
  • 1
    This is already being done, and works on all other endpoints. It's the single endpoint that is outside of my codebase (handled by an external service within the App Engine environment) that is not providing these headers. – seawolf Mar 12 '16 at 20:45