最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Issues with CORS. Flask <-> AngularJS - Stack Overflow

programmeradmin1浏览0评论

Starting a new project with a angularjs client app and a flask app providing the api. I'm using mongodb as the database. I had to immediately rule out jsonp since I would need the ability to POST across different ports. So we have localhost:9000 for the angular app and localhost:9001 for the flask app.

I went through and made the changed needed for CORS in my API as well as my angular files. See source below. First issue I ran in to was that there is a bug that CORS allow header does not recognize localhost in Chrome. I updated my hosts file so I could use moneybooks.dev and this worked for my GET requests without using JSONP.

Now, to the issues I'm facing. When submitting a POST request, its stating Origin :9000 is not allowed by Access-Control-Allow-Origin What? GET can go through but POST is declined. I see the request e through to flask but it returns HTTP 400. I need help making POST requests work.

Another issue, which may be related, is that on my GET requests, sometimes the GET request doesn't fire at all. Like in BudgetCtrl the loadBudget function. On #/budgets/budgetID the name of the budget will sometimes not load at all. I check the flask log and don't see a request ing through. Then I click refresh, I see the request, the budget name appears on the page however in the flask log I see an error. [Errno 10053] An established connection was aborted by the software in your host machine. Its a connection error that only appears in the flask log when the GET request succeeds.

Are these issues related? Can anyone see what I'm doing wrong?

app.js

'use strict';

angular.module('MoneybooksApp', ['ui.bootstrap', 'ngResource'])
  .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) {
    $httpProvider.defaults.useXDomain = true;
    delete $httpProvider.defaults.headersmon['X-Requested-With'];
    $routeProvider
      .when('/', {
        templateUrl: 'views/main.html',
        controller: 'MainCtrl'
      })
      .otherwise({
        redirectTo: '/'
      });
  }]);

budgets.js

'use strict';

angular.module('MoneybooksApp')
  .config(['$routeProvider', function ($routeProvider) {
    $routeProvider
      .when('/budgets', {
        templateUrl: 'views/budgets-list.html',
        controller: 'BudgetListCtrl'
      })
      .when('/budgets/:budgetID', {
        templateUrl: 'views/budget.html',
        controller: 'BudgetCtrl'
      });
  }])
  .controller('BudgetListCtrl', function ($scope, $http, $resource) {
    $scope.budgets = [];

    var init = function () {
      $scope.loadBudgets();
    }

    $scope.loadBudgets = function() {
      $http.get(':9001/api/budgets')
        .success(function (data) {
          $scope.budgets = data;
        })
        .error(function (data) {
          console.error(data);
        });
    };

    init();
  })
  .controller('BudgetCtrl', function ($scope, $http, $routeParams, $resource) {
    $scope.budget = {};

    var init = function () {
      $scope.loadBudget();
    };

    $scope.loadBudget = function() {
      $http.get(':9001/api/budgets/'+$routeParams['budgetID'])
        .success(function (data) {
          $scope.budget = data;
        })
        .error(function (data) {
          console.error(data);
        });
    };

    init();
  })
  .controller('TransactionCtrl', function ($scope, $http, $routeParams, $resource) {
    $scope.transactions = [];
    $scope.editing = false;
    $scope.editingID;

    var init = function () {};


    $scope.syncUp = function () {
      $http.post(':9001/api/budgets/'+$routeParams['budgetID']+'/transactions', {transactions: $scope.transactions});
    };

    $scope.syncDown = function () {
      $http.get(':9001/api/budgets/'+$$routeParams['budgetID']+'/transactions')
        .success(function (transactions) {
          $scope.transactions = transactions;
        });
    };

    $scope.add = function() {
      $scope.transactions.push({
        amount: $scope.amount,
        description: $scope.description,
        datetime: $scope.datetime
      });

      reset();
      $scope.defaultSort();
    };

    $scope.edit = function(index) {
      var transaction = $scope.transactions[index];

      $scope.amount = transaction.amount;
      $scope.description = transaction.description;
      $scope.datetime = transaction.datetime;

      $scope.inserting = false;
      $scope.editing = true;
      $scope.editingID = index;
    };

    $scope.save = function() {
      $scope.transactions[$scope.editingID].amount = $scope.amount;
      $scope.transactions[$scope.editingID].description = $scope.description;
      $scope.transactions[$scope.editingID].datetime = $scope.datetime;

      reset();
      $scope.defaultSort();
    };

    var reset = function() {
      $scope.editing = false;
      $scope.editingID = undefined;

      $scope.amount = '';
      $scope.description = '';
      $scope.datetime = '';
    };

    $scope.cancel = function() {
      reset();
    };


    $scope.remove = function(index) {
      $scope.transactions.splice(index, 1);
      if ($scope.editing) {
        reset();
      }
    };

    $scope.defaultSort = function() {
      var sortFunction = function(a, b) {
        var a_date = new Date(a['datetime']);
        var b_date = new Date(b['datetime']);

        if (a['datetime'] === b['datetime']) {
          var x = a['amount'], y = b['amount'];
          return x > y ? -1 : x < y ? 1 : 0;
        } else {
          return a_date - b_date
        }
      };

      $scope.transactions.sort(sortFunction);
    };

    $scope.descriptionSuggestions = function() {
      var suggestions = [];

      return $.map($scope.transactions, function(transaction) {
        if ($.inArray(transaction.description, suggestions) === -1){
          suggestions.push(transaction.description);
          return transaction.description;
        }
      });
    };

    $scope.dateSuggestions = function () {
      var suggestions = [];

      return $.map($scope.transactions, function(transaction) {
        if ($.inArray(transaction.datetime, suggestions) === -1){
          suggestions.push(transaction.datetime);
          return transaction.datetime;
        }
      });
    }

    $scope.getRunningTotal = function(index) {
      var runningTotal = 0;
      var selectedTransactions = $scope.transactions.slice(0, index+1);
      angular.forEach(selectedTransactions, function(transaction, index){
        runningTotal += transaction.amount;
      });
      return runningTotal;
    };

    init();

    $(function(){
      (function($){
        var header = $('#budget-header');
        var budget = $('#budget');
        var pos = header.offset();

        $(window).scroll(function(){
          if ($(this).scrollTop() > pos.top && header.css('position') == 'static') {
            header.css({
              position: 'fixed',
              width: header.width(),
              top: 0
            }).addClass('pinned');
            budget.css({
              'margin-top': '+='+header.height()
            });
          } else if ($(this).scrollTop() < pos.top && header.css('position') == 'fixed') {
            header.css({
              position: 'static'
            }).removeClass('pinned');
            budget.css({
              'margin-top': '-='+header.height()
            });
          }
        });
      })(jQuery);
    });
  });

API.py

from flask import Flask, Response, Blueprint, request
from pymongo import MongoClient
from bson.json_util import dumps
from decorators import crossdomain
from bson.objectid import ObjectId

try:
    import json
except ImportError:
    import simplejson as json

class APIEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, objectid.ObjectID):
            return str(obj)

app = Flask(__name__)

client = MongoClient()
db = client['moneybooks']

api = Blueprint('api', __name__, url_prefix="/api")

@api.route('/budgets', methods=['GET', 'POST', 'OPTIONS'])
@crossdomain(origin='*', methods=['GET', 'POST', 'OPTIONS'], headers=['X-Requested-With', 'Content-Type', 'Origin'])
def budgets():
    if request.method == "POST":
        budget_id = db.budgets.insert({
            'name': request.form['name']
        })
        budget_json = dumps(db.budgets.find_one({'_id': budget_id}), cls=APIEncoder)

    if request.method == "GET":
        budget_json = dumps(db.budgets.find(), cls=APIEncoder)

    return Response(budget_json, mimetype='application/json')

@api.route('/budgets/<budget_id>', methods=['GET', 'OPTIONS'])
@crossdomain(origin='*', methods=['GET', 'OPTIONS'], headers=['X-Requested-With', 'Content-Type', 'Origin'])
def budget(budget_id):
  budget_json = dumps(db.budgets.find_one({'_id': ObjectId(budget_id)}), cls=APIEncoder)
  return Response(budget_json, mimetype='application/json')

@api.route('/budgets/<budget_id>/transactions', methods=['GET', 'POST', 'OPTIONS'])
@crossdomain(origin='*', methods=['GET', 'POST', 'OPTIONS'], headers=['X-Requested-With', 'Content-Type', 'Origin'])
def transactions(budget_id):
    if request.method == "POST":
        db.budgets.update({
            '_id': ObjectId(budget_id)
        }, {
            '$set': {
                'transactions': request.form['transactions']
            }
        });
        budget_json = dumps(db.budgets.find_one({'_id': ObjectId(budget_id)}), cls=APIEncoder)

    if request.method == "GET":
        budget_json = dumps(db.budgets.find_one({'_id': ObjectId(budget_id)}).transactions, cls=APIEncoder)

    return Response(budget_json, mimetype='application/json')

app.register_blueprint(api)

if __name__ == '__main__':
    app.config['debug'] = True
    app.config['PROPAGATE_EXCEPTIONS'] = True
    app.run()

decorators.py

from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper

def crossdomain(origin=None, methods=None, headers=None, max_age=21600, attach_to_all=True, automatic_options=True):
    if methods is not None:
        methods = ', '.join(sorted(x.upper() for x in methods))
    if headers is not None and not isinstance(headers, basestring):
        headers = ', '.join(x.upper() for x in headers)
    if isinstance(max_age, timedelta):
        max_age = max_age.total_seconds()

    def get_methods():
        if methods is not None:
            return methods

        options_resp = current_app.make_default_options_response()
        return options_resp.headers['allow']

    def decorator(f):
        def wrapped_function(*args, **kwargs):
            if automatic_options and request.method == 'OPTIONS':
                resp = current_app.make_default_options_response()
            else:
                resp = make_response(f(*args, **kwargs))
            if not attach_to_all and request.method != 'OPTIONS':
                return resp

            h = resp.headers
            h['Access-Control-Allow-Origin'] = origin
            h['Access-Control-Allow-Methods'] = get_methods()
            h['Access-Control-Max-Age'] = str(max_age)

            if headers is not None:
                h['Access-Control-Allow-Headers'] = headers
            return resp

        f.provide_automatic_options = False
        f.required_methods = ['OPTIONS']
        return update_wrapper(wrapped_function, f)
    return decorator

Edit

Output from chrome dev console.

Console:

XMLHttpRequest cannot load :9001/api/budgets/5223e780f58e4d20509b4b8b/transactions. Origin :9000 is not allowed by Access-Control-Allow-Origin.

Network

Name: transactions /api/budgets/5223e780f58e4d20509b4b8b
Method: POST
Status: (canceled)
Type: Pending
Initiator: angular.js:9499
Size: 13 B / 0 B
Latency: 21 ms

Starting a new project with a angularjs client app and a flask app providing the api. I'm using mongodb as the database. I had to immediately rule out jsonp since I would need the ability to POST across different ports. So we have localhost:9000 for the angular app and localhost:9001 for the flask app.

I went through and made the changed needed for CORS in my API as well as my angular files. See source below. First issue I ran in to was that there is a bug that CORS allow header does not recognize localhost in Chrome. I updated my hosts file so I could use moneybooks.dev and this worked for my GET requests without using JSONP.

Now, to the issues I'm facing. When submitting a POST request, its stating Origin http://moneybooks.dev:9000 is not allowed by Access-Control-Allow-Origin What? GET can go through but POST is declined. I see the request e through to flask but it returns HTTP 400. I need help making POST requests work.

Another issue, which may be related, is that on my GET requests, sometimes the GET request doesn't fire at all. Like in BudgetCtrl the loadBudget function. On #/budgets/budgetID the name of the budget will sometimes not load at all. I check the flask log and don't see a request ing through. Then I click refresh, I see the request, the budget name appears on the page however in the flask log I see an error. [Errno 10053] An established connection was aborted by the software in your host machine. Its a connection error that only appears in the flask log when the GET request succeeds.

Are these issues related? Can anyone see what I'm doing wrong?

app.js

'use strict';

angular.module('MoneybooksApp', ['ui.bootstrap', 'ngResource'])
  .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) {
    $httpProvider.defaults.useXDomain = true;
    delete $httpProvider.defaults.headers.mon['X-Requested-With'];
    $routeProvider
      .when('/', {
        templateUrl: 'views/main.html',
        controller: 'MainCtrl'
      })
      .otherwise({
        redirectTo: '/'
      });
  }]);

budgets.js

'use strict';

angular.module('MoneybooksApp')
  .config(['$routeProvider', function ($routeProvider) {
    $routeProvider
      .when('/budgets', {
        templateUrl: 'views/budgets-list.html',
        controller: 'BudgetListCtrl'
      })
      .when('/budgets/:budgetID', {
        templateUrl: 'views/budget.html',
        controller: 'BudgetCtrl'
      });
  }])
  .controller('BudgetListCtrl', function ($scope, $http, $resource) {
    $scope.budgets = [];

    var init = function () {
      $scope.loadBudgets();
    }

    $scope.loadBudgets = function() {
      $http.get('http://moneybooks.dev:9001/api/budgets')
        .success(function (data) {
          $scope.budgets = data;
        })
        .error(function (data) {
          console.error(data);
        });
    };

    init();
  })
  .controller('BudgetCtrl', function ($scope, $http, $routeParams, $resource) {
    $scope.budget = {};

    var init = function () {
      $scope.loadBudget();
    };

    $scope.loadBudget = function() {
      $http.get('http://moneybooks.dev:9001/api/budgets/'+$routeParams['budgetID'])
        .success(function (data) {
          $scope.budget = data;
        })
        .error(function (data) {
          console.error(data);
        });
    };

    init();
  })
  .controller('TransactionCtrl', function ($scope, $http, $routeParams, $resource) {
    $scope.transactions = [];
    $scope.editing = false;
    $scope.editingID;

    var init = function () {};


    $scope.syncUp = function () {
      $http.post('http://moneybooks.dev:9001/api/budgets/'+$routeParams['budgetID']+'/transactions', {transactions: $scope.transactions});
    };

    $scope.syncDown = function () {
      $http.get('http://moneybooks.dev:9001/api/budgets/'+$$routeParams['budgetID']+'/transactions')
        .success(function (transactions) {
          $scope.transactions = transactions;
        });
    };

    $scope.add = function() {
      $scope.transactions.push({
        amount: $scope.amount,
        description: $scope.description,
        datetime: $scope.datetime
      });

      reset();
      $scope.defaultSort();
    };

    $scope.edit = function(index) {
      var transaction = $scope.transactions[index];

      $scope.amount = transaction.amount;
      $scope.description = transaction.description;
      $scope.datetime = transaction.datetime;

      $scope.inserting = false;
      $scope.editing = true;
      $scope.editingID = index;
    };

    $scope.save = function() {
      $scope.transactions[$scope.editingID].amount = $scope.amount;
      $scope.transactions[$scope.editingID].description = $scope.description;
      $scope.transactions[$scope.editingID].datetime = $scope.datetime;

      reset();
      $scope.defaultSort();
    };

    var reset = function() {
      $scope.editing = false;
      $scope.editingID = undefined;

      $scope.amount = '';
      $scope.description = '';
      $scope.datetime = '';
    };

    $scope.cancel = function() {
      reset();
    };


    $scope.remove = function(index) {
      $scope.transactions.splice(index, 1);
      if ($scope.editing) {
        reset();
      }
    };

    $scope.defaultSort = function() {
      var sortFunction = function(a, b) {
        var a_date = new Date(a['datetime']);
        var b_date = new Date(b['datetime']);

        if (a['datetime'] === b['datetime']) {
          var x = a['amount'], y = b['amount'];
          return x > y ? -1 : x < y ? 1 : 0;
        } else {
          return a_date - b_date
        }
      };

      $scope.transactions.sort(sortFunction);
    };

    $scope.descriptionSuggestions = function() {
      var suggestions = [];

      return $.map($scope.transactions, function(transaction) {
        if ($.inArray(transaction.description, suggestions) === -1){
          suggestions.push(transaction.description);
          return transaction.description;
        }
      });
    };

    $scope.dateSuggestions = function () {
      var suggestions = [];

      return $.map($scope.transactions, function(transaction) {
        if ($.inArray(transaction.datetime, suggestions) === -1){
          suggestions.push(transaction.datetime);
          return transaction.datetime;
        }
      });
    }

    $scope.getRunningTotal = function(index) {
      var runningTotal = 0;
      var selectedTransactions = $scope.transactions.slice(0, index+1);
      angular.forEach(selectedTransactions, function(transaction, index){
        runningTotal += transaction.amount;
      });
      return runningTotal;
    };

    init();

    $(function(){
      (function($){
        var header = $('#budget-header');
        var budget = $('#budget');
        var pos = header.offset();

        $(window).scroll(function(){
          if ($(this).scrollTop() > pos.top && header.css('position') == 'static') {
            header.css({
              position: 'fixed',
              width: header.width(),
              top: 0
            }).addClass('pinned');
            budget.css({
              'margin-top': '+='+header.height()
            });
          } else if ($(this).scrollTop() < pos.top && header.css('position') == 'fixed') {
            header.css({
              position: 'static'
            }).removeClass('pinned');
            budget.css({
              'margin-top': '-='+header.height()
            });
          }
        });
      })(jQuery);
    });
  });

API.py

from flask import Flask, Response, Blueprint, request
from pymongo import MongoClient
from bson.json_util import dumps
from decorators import crossdomain
from bson.objectid import ObjectId

try:
    import json
except ImportError:
    import simplejson as json

class APIEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, objectid.ObjectID):
            return str(obj)

app = Flask(__name__)

client = MongoClient()
db = client['moneybooks']

api = Blueprint('api', __name__, url_prefix="/api")

@api.route('/budgets', methods=['GET', 'POST', 'OPTIONS'])
@crossdomain(origin='*', methods=['GET', 'POST', 'OPTIONS'], headers=['X-Requested-With', 'Content-Type', 'Origin'])
def budgets():
    if request.method == "POST":
        budget_id = db.budgets.insert({
            'name': request.form['name']
        })
        budget_json = dumps(db.budgets.find_one({'_id': budget_id}), cls=APIEncoder)

    if request.method == "GET":
        budget_json = dumps(db.budgets.find(), cls=APIEncoder)

    return Response(budget_json, mimetype='application/json')

@api.route('/budgets/<budget_id>', methods=['GET', 'OPTIONS'])
@crossdomain(origin='*', methods=['GET', 'OPTIONS'], headers=['X-Requested-With', 'Content-Type', 'Origin'])
def budget(budget_id):
  budget_json = dumps(db.budgets.find_one({'_id': ObjectId(budget_id)}), cls=APIEncoder)
  return Response(budget_json, mimetype='application/json')

@api.route('/budgets/<budget_id>/transactions', methods=['GET', 'POST', 'OPTIONS'])
@crossdomain(origin='*', methods=['GET', 'POST', 'OPTIONS'], headers=['X-Requested-With', 'Content-Type', 'Origin'])
def transactions(budget_id):
    if request.method == "POST":
        db.budgets.update({
            '_id': ObjectId(budget_id)
        }, {
            '$set': {
                'transactions': request.form['transactions']
            }
        });
        budget_json = dumps(db.budgets.find_one({'_id': ObjectId(budget_id)}), cls=APIEncoder)

    if request.method == "GET":
        budget_json = dumps(db.budgets.find_one({'_id': ObjectId(budget_id)}).transactions, cls=APIEncoder)

    return Response(budget_json, mimetype='application/json')

app.register_blueprint(api)

if __name__ == '__main__':
    app.config['debug'] = True
    app.config['PROPAGATE_EXCEPTIONS'] = True
    app.run()

decorators.py

from datetime import timedelta
from flask import make_response, request, current_app
from functools import update_wrapper

def crossdomain(origin=None, methods=None, headers=None, max_age=21600, attach_to_all=True, automatic_options=True):
    if methods is not None:
        methods = ', '.join(sorted(x.upper() for x in methods))
    if headers is not None and not isinstance(headers, basestring):
        headers = ', '.join(x.upper() for x in headers)
    if isinstance(max_age, timedelta):
        max_age = max_age.total_seconds()

    def get_methods():
        if methods is not None:
            return methods

        options_resp = current_app.make_default_options_response()
        return options_resp.headers['allow']

    def decorator(f):
        def wrapped_function(*args, **kwargs):
            if automatic_options and request.method == 'OPTIONS':
                resp = current_app.make_default_options_response()
            else:
                resp = make_response(f(*args, **kwargs))
            if not attach_to_all and request.method != 'OPTIONS':
                return resp

            h = resp.headers
            h['Access-Control-Allow-Origin'] = origin
            h['Access-Control-Allow-Methods'] = get_methods()
            h['Access-Control-Max-Age'] = str(max_age)

            if headers is not None:
                h['Access-Control-Allow-Headers'] = headers
            return resp

        f.provide_automatic_options = False
        f.required_methods = ['OPTIONS']
        return update_wrapper(wrapped_function, f)
    return decorator

Edit

Output from chrome dev console.

Console:

XMLHttpRequest cannot load http://moneybooks.dev:9001/api/budgets/5223e780f58e4d20509b4b8b/transactions. Origin http://moneybooks.dev:9000 is not allowed by Access-Control-Allow-Origin.

Network

Name: transactions /api/budgets/5223e780f58e4d20509b4b8b
Method: POST
Status: (canceled)
Type: Pending
Initiator: angular.js:9499
Size: 13 B / 0 B
Latency: 21 ms
Share Improve this question edited Sep 7, 2013 at 15:56 Kylee asked Sep 3, 2013 at 2:38 KyleeKylee 1,6752 gold badges32 silver badges55 bronze badges 10
  • Can you post the request/response headers for a post request. They will be more relevant than source code. Also have you tried posting to your server using curl? That is often helpful in diagnosing problems. – Jason Commented Sep 4, 2013 at 2:28
  • I sent a POST request using a chrome extension Dev HTTP Client, which allows you generate HTTP requests. Those POST requests work fine and insert in to the DB. Not sure where to get the headers you're looking for. Here is what I see in the Flask log output: 127.0.0.1 -- [02/Sep/2013 21:30] "OPTIONS /api/budgets/<budgetID>/transactions HTTP1.1" 200 - 127.0.0.1 -- [02/Sep/2013 21:30] "POST/api/budgets/<budgetID>/transactions HTTP1.1" 400 – Kylee Commented Sep 4, 2013 at 2:49
  • Chrome Dev tools, network panel -developers.google./chrome-developer-tools – Jason Commented Sep 4, 2013 at 2:51
  • I edited my question with that information. I find it odd that its canceled/pending? – Kylee Commented Sep 4, 2013 at 2:58
  • 2 Its worth noting that Chrome Dev Tools has a 'bug' where it will show that CORS failed when the server returns an error. It will never show the error and you may think it had something to do with CORS. Double check, use Firebug. Firebug will show the server's response that fails CORS. – TheSharpieOne Commented Sep 9, 2013 at 19:09
 |  Show 5 more ments

3 Answers 3

Reset to default 5 +100

As @TheSharpieOne pointed out, the CORS error is likely a red herring caused by a Chrome Dev Tools bug. If it was an actual CORS issue, the pre-flight OPTIONS call should have returned the same error.

I believe your 400 error may be ing from request.form['transactions'] in the handler for the POST request. request.form is a MultiDict datastructure and according to the documentation at http://werkzeug.pocoo/docs/datastructures/#werkzeug.datastructures.MultiDict:

From Werkzeug 0.3 onwards, the KeyError raised by this class is also a subclass of the BadRequest HTTP exception and will render a page for a 400 BAD REQUEST if caught in a catch-all for HTTP exceptions.

I believe that if you check for the 'transactions' key in request.forms.keys(), you'll find that it does not exist. Note that the content type for the POST is application/json not x-www-form-urlencoded. According to the documentation at http://flask.pocoo/docs/api/#flask.Request.get_json, you'll want to get the request data using the request.get_json() function when the request mimetype is application/json.

Is the POST sending content ? I had a similar issuee when the body was null. If it is, adding either an empty body ("") when the object is falsy, or adding the ContentLength header as 0 both seemed to work.

$scope.syncUp = function () {
var objToSend = $scope.transactions ? { transactions: $scope.transactions } : "";
$http.post('http://moneybooks.dev:9001/api/budgets/'+$routeParams['budgetID']+'/transactions', objToSend);
};

Ensure app.js is included before budget.js in your HTML page

发布评论

评论列表(0)

  1. 暂无评论