CORS 的问题。烧瓶 <-> AngularJS

Posted

技术标签:

【中文标题】CORS 的问题。烧瓶 <-> AngularJS【英文标题】:Issues with CORS. Flask <-> AngularJS 【发布时间】:2013-09-06 03:45:00 【问题描述】:

使用 angularjs 客户端应用程序和提供 api 的烧瓶应用程序开始一个新项目。我使用 mongodb 作为数据库。我必须立即排除 jsonp,因为我需要能够跨不同端口进行 POST。所以我们有 localhost:9000 用于 Angular 应用程序和 localhost:9001 用于烧瓶应用程序。

我在我的 API 以及我的 Angular 文件中完成了 CORS 所需的更改。请参阅下面的来源。我遇到的第一个问题是 CORS 允许标头无法识别 Chrome 中的 localhost 的错误。我更新了我的主机文件,以便我可以使用 moneybooks.dev,这适用于我的 GET 请求,而无需使用 JSONP。

现在,谈谈我面临的问题。提交 POST 请求时,它声明 Origin http://moneybooks.dev:9000 is not allowed by Access-Control-Allow-Origin 什么? GET 可以通过,但 POST 被拒绝。我看到请求通过烧瓶,但它返回 HTTP 400。我需要帮助使 POST 请求正常工作。

另一个可能相关的问题是,在我的 GET 请求中,有时 GET 请求根本不会触发。就像在 BudgetCtrl 中的 loadBudget 函数一样。在 #/budgets/budgetID 上,有时根本不会加载预算名称。我检查了烧瓶日志,没有看到通过的请求。然后我单击刷新,我看到了请求,预算名称出现在页面上但是在烧瓶日志中我看到了一个错误。 [Errno 10053] An established connection was aborted by the software in your host machine.这是一个连接错误,只有在GET请求成功时才会出现在flask日志中。

这些问题是否相关?谁能看到我做错了什么?

app.js

'use strict';

angular.module('MoneybooksApp', ['ui.bootstrap', 'ngResource'])
  .config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) 
    $httpProvider.defaults.useXDomain = true;
    delete $httpProvider.defaults.headers.common['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

编辑

来自 chrome 开发控制台的输出。

控制台:

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

网络

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

【问题讨论】:

您能否发布发布请求的请求/响应标头。它们将比源代码更相关。您还尝试过使用 curl 发布到您的服务器吗?这通常有助于诊断问题。 我使用 chrome 扩展 Dev HTTP Client 发送了一个 POST 请求,它允许您生成 HTTP 请求。这些 POST 请求工作正常并插入数据库。不知道从哪里获得您正在寻找的标题。这是我在 Flask 日志输出中看到的内容:127.0.0.1 -- [02/Sep/2013 21:30] "OPTIONS /api/budgets/&lt;budgetID&gt;/transactions HTTP1.1" 200 - 127.0.0.1 -- [02/Sep/2013 21:30] "POST/api/budgets/&lt;budgetID&gt;/transactions HTTP1.1" 400 Chrome 开发工具,网络面板 -developers.google.com/chrome-developer-tools 我用这些信息编辑了我的问题。我觉得它取消/待定很奇怪? 值得注意的是,Chrome 开发工具有一个“错误”,当服务器返回错误时,它会显示 CORS 失败。它永远不会显示错误,您可能认为它与 CORS 有关。仔细检查,使用 Firebug。 Firebug 将显示 CORS 失败的服务器响应。 【参考方案1】:

正如@TheSharpieOne 指出的那样,CORS 错误很可能是由 Chrome 开发工具错误引起的红鲱鱼。如果这是一个实际的 CORS 问题,则飞行前 OPTIONS 调用应该返回相同的错误。

我相信您的 400 错误可能来自 POST 请求处理程序中的 request.form['transactions']request.form 是一个 MultiDict 数据结构,根据 http://werkzeug.pocoo.org/docs/datastructures/#werkzeug.datastructures.MultiDict 的文档:

从 Werkzeug 0.3 开始,此类引发的 KeyError 也是 BadRequest HTTP 异常的子类,如果在 HTTP 异常的全部捕获中捕获,则会呈现 400 BAD REQUEST 的页面。

我相信如果您检查request.forms.keys() 中的'transactions' 键,您会发现它不存在。请注意,POST 的内容类型是 application/json 而不是 x-www-form-urlencoded。根据http://flask.pocoo.org/docs/api/#flask.Request.get_json 的文档,当请求mimetype 为application/json 时,您需要使用request.get_json() 函数获取请求数据。

【讨论】:

【参考方案2】:

POST 是否发送内容?当身体为空时,我有一个类似的问题。如果是这样,当对象为假时添加一个空主体(“”),或者将 ContentLength 标头添加为 0 似乎都有效。

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

【讨论】:

这就是我的想法,但 $scope.transactions 在控制器顶部设置为 [] ,因此即使它是一个空数组,它仍然应该发送一些东西。我确实尝试了您的代码,但仍然遇到 moneybooks.dev:9000 is not allowed by origin 的相同错误。【参考方案3】:

确保在 HTML 页面中 app.js 包含在 budget.js 之前

【讨论】:

在budget.js 之前。除 POST 请求外,一切正常。

以上是关于CORS 的问题。烧瓶 <-> AngularJS的主要内容,如果未能解决你的问题,请参考以下文章

烧瓶 socketio CORS

烧瓶 Cors 不工作

使用 Socket.IO 反应和烧瓶 - CORS 问题

Axios 发布请求被烧瓶制作的 api 拒绝。 CORS错误即将出现[重复]

从烧瓶 api 获取参数

接收空表单的烧瓶