Flask CORS 仅适用于第一个请求,我的代码中有什么错误?

Flask CORS work only for first request, what's the bug in my code?

背景

  1. 有一个 JS 应用服务于 127.0.0.1:8080,它指的是一些 API 服务于 127.0.0.1:5000 的 Flask 应用。 [参见 FlaskCode]

  2. 当我在 Chrome 中打开此 js 应用程序时,第一个请求运行良好,第二个请求以 CORS 问题结束,[参见 ChromeDebug1]。

  3. 此外,我发现这个'OPTIONS'在Flask输出中是405(方法不允许)的响应,flask_cors的输出与第一个请求不同。 [参见 FlaskOut]。

  4. 我是FE新手python,如果是愚蠢的错误,请告诉我。

  5. 我的环境是

MacOs M1 version11.1
Chrome Version 87.0
Python 3.8.2
Flask 2.1.1
Werkzeug 2.1.1

问题

  1. 似乎 flask_cors 在我的代码中只工作了一次,但是有什么问题吗?

  2. 看看第一个req和第二个req的区别,好像'OPTIONS'的第二个reponse没有headers ("Access-Control-Allow-Origin", "*")?

  3. 为什么第一次请求没有像flask_cors.extension:Request

    这样的日志

======第二次编辑=========

感谢大卫的建议。我使用 tcp dump 来捕获网络,[参见 wireshark]。这个 OPTION 请求在我看来是标准的。所以,这让我想到了问题 4。

  1. 为什么 flask 打印 "{"examinationOID":"61e8d2248373a7329e12f29b"}OPTIONS /yd/pass-through/get-examination HTTP/1.1" 405 - 而请求没有正文?也许打印是来自上次请求的垃圾对象,由于长连接和异常处理而未正确 gc?

我只有一个 python 文件,并且 运行 它与 python ./demo2.py --log=INFO

附录

FlaskCode

# -*- coding: UTF-8 -*-

from flask import Flask
from flask import Response
from flask_cors import CORS, cross_origin
import logging
import json

app = Flask(__name__)
CORS(app, supports_credentials=True)

demoDataPath="xxx"

@app.route("/yd/pass-through/get-examination", methods=['POST'])
@cross_origin()
def getexamination():
    logging.getLogger('demo2').info('into getexamination')
    response = {}
    response["code"]=0
    response["message"]="good end"
    f = open(demoDataPath+"/rsp4getexamination.json", "r")
    response["data"]= json.loads(f.read())
    return Response(json.dumps(response), mimetype='application/json', status=200) 

@app.route("/yd/pass-through/report-config", methods=['POST'])
@cross_origin()
def getconfig():
    logging.getLogger('demo2').info('into getconfig')
    response = {}
    response["code"]=0
    response["message"]="good end"
    f = open(demoDataPath+"/rsp4getreportconfig.json", "r")
    response["data"]= json.loads(f.read())
    return Response(json.dumps(response), mimetype='application/json', status=200) 

if __name__ == '__main__':
    logging.getLogger('flask_cors').level = logging.DEBUG
    logging.getLogger('werkzeug').level = logging.DEBUG
    logging.getLogger('demo2').level = logging.DEBUG
    app.logger.setLevel(logging.DEBUG)
    logging.info("app run")
    app.run(debug=True, threaded=True, port=5001)

Chrome调试 1

FlaskOut

DEBUG:flask_cors.core:CORS request received with 'Origin' http://127.0.0.1:8080
DEBUG:flask_cors.core:The request's Origin header matches. Sending CORS headers.
DEBUG:flask_cors.core:Settings CORS headers: MultiDict([('Access-Control-Allow-Origin', 'http://127.0.0.1:8080'), ('Access-Control-Allow-Headers', 'content-type, traceid, withcredentials'), ('Access-Control-Allow-Methods', 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT'), ('Vary', 'Origin')])
DEBUG:flask_cors.extension:CORS have been already evaluated, skipping
INFO:werkzeug:127.0.0.1 - - [21/Apr/2022 20:33:36] "OPTIONS /yd/pass-through/report-config HTTP/1.1" 200 -
[2022-04-21 20:33:36,736] INFO in demo2: into getconfig
INFO:demo2:into getconfig
DEBUG:flask_cors.core:CORS request received with 'Origin' http://127.0.0.1:8080
DEBUG:flask_cors.core:The request's Origin header matches. Sending CORS headers.
DEBUG:flask_cors.core:Settings CORS headers: MultiDict([('Access-Control-Allow-Origin', 'http://127.0.0.1:8080'), ('Vary', 'Origin')])
DEBUG:flask_cors.extension:CORS have been already evaluated, skipping
INFO:werkzeug:127.0.0.1 - - [21/Apr/2022 20:33:36] "POST /yd/pass-through/report-config HTTP/1.1" 200 -
DEBUG:flask_cors.extension:Request to '/yd/pass-through/get-examination' matches CORS resource '/*'. Using options: {'origins': ['.*'], 'methods': 'DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT', 'allow_headers': ['.*'], 'expose_headers': None, 'supports_credentials': True, 'max_age': None, 'send_wildcard': False, 'automatic_options': True, 'vary_header': True, 'resources': '/*', 'intercept_exceptions': True, 'always_send': True}
DEBUG:flask_cors.core:CORS request received with 'Origin' http://127.0.0.1:8080
DEBUG:flask_cors.core:The request's Origin header matches. Sending CORS headers.
DEBUG:flask_cors.core:Settings CORS headers: MultiDict([('Access-Control-Allow-Origin', 'http://127.0.0.1:8080'), ('Access-Control-Allow-Credentials', 'true'), ('Vary', 'Origin')])
DEBUG:flask_cors.extension:CORS have been already evaluated, skipping
INFO:werkzeug:127.0.0.1 - - [21/Apr/2022 20:33:36] "{"examinationOID":"61e8d2248373a7329e12f29b"}OPTIONS /yd/pass-through/get-examination HTTP/1.1" 405 -

wireshark

我用另一个“错误”解决了我的问题,如下所示。而且我仍然不知道为什么我的代码不能像 flask_cors 文档所说的那样工作。如果您有更好的解决方案或建议,请告诉我。

@app.after_request
def add_headers(response):
    response.headers.add('Content-Type', 'application/json')
    response.headers.add('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
    response.headers.add('Access-Control-Allow-Headers', 'content-type, traceid, withcredentials')
    response.status=200
    logging.getLogger('demo2').info('into add_headers')
    return response

问题似乎出在这里:

INFO:werkzeug:127.0.0.1 - - [21/Apr/2022 20:33:36] "{"examinationOID":"61e8d2248373a7329e12f29b"}OPTIONS /yd/pass-through/get-examination HTTP/1.1" 405 -

比较一下:

INFO:werkzeug:127.0.0.1 - - [21/Apr/2022 20:33:36] "OPTIONS /yd/pass-through/report-config HTTP/1.1" 200 -

它正在尝试使用无效的 HTTP 方法 {"examinationOID":"61e8d2248373a7329e12f29b"}OPTIONS 而不是普通的 OPTIONS

似乎有什么东西正在破坏您的 CORS headers 并使 Chrome 迷惑不解。一个可能的来源是 Flask-CORS 配置。其他地方是否有一些您没有向我们展示的设置?还是只是使用默认配置?

如果它只是默认值,那么查看 Chrome 中的 Javascript 代码 运行 会有所帮助 - 这可能导致从第一个请求到第二个请求的方法损坏.

此外,在您的 chrome 开发工具屏幕截图中,您正在过滤 'XHR' - 不会显示 OPTIONS 请求,这将帮助您找出问题所在。它们显示在 'Other'.

我遇到了类似的错误: param1=value1&param2=value2GET /css/base.css HTTP/1.1 其中前导参数来自之前调用的 POST 请求。设置 threaded=False(如@david-k-hess 所建议的那样)有帮助。

整个故事是:

  • 浏览器使用 POST
  • 提交表单
  • Flask 服务器响应网页
  • 该网页的头部包含 /css/base.css
  • 浏览器下载 /css/base.css 使用 GET
  • Flask 服务器 returns HTTP 405 因为它认为方法是 param1=value1&param2=value2GET

修复

@david-k-hess 建议的修复 1:在 Flask app.run() 中,添加 threaded=False

修复 2:似乎通过将 Flask 和 Werkzeug 升级到 2.1.2 也解决了这个问题

我的环境:

  • Python 3.9
  • 烧瓶 2.1.1
  • Werkzeug 2.1.1

除此之外,我的 venv 包含:

  • Jinja2
  • 标记安全
  • 点击
  • 彩色
  • importlib-metadata
  • 很危险
  • 点子
  • 设置工具
  • 车轮
  • 齐普