corydolphin / flask-cors

Cross Origin Resource Sharing ( CORS ) support for Flask
https://flask-cors.corydolphin.com/
MIT License
873 stars 140 forks source link

Decorator only applies to the first route declared #280

Closed marzetas closed 1 year ago

marzetas commented 3 years ago

Hi, I was wondering if you could help with an issue I'm observing while using the cross_origin decorator. I have two endpoints linked to the same route, one serving GET requests, the other serving POST requests. Something like this:

from flask_cors import cross_origin

@app.route("/", methods=['GET'])
def get_item():
      return 'foo'

@app.route("/", methods=['POST'])
@cross_origin()
def create_item():
      return 'bar'

What happens here is that the cross_origin decorator is never hit when I hit the POST endpoint.

From what I can see, it seems like the decorator is linked to the route, and only the first route declared keeps the cross_origin decorator. I'm saying this because altering the order of the declaration of the endpoints makes the POST one to fire the cross origin decorator. Basically this works:

from flask_cors import cross_origin

@app.route("/", methods=['POST'])
@cross_origin()
def create_item():
      return 'bar'

@app.route("/", methods=['GET'])
def get_item():
      return 'foo'

Any ideas? The code works in this second iteration, but it's not ideal that the order of the declarations of the endpoints affects the cross origin decorator.

Thanks in advance!

pylipp commented 1 year ago

Wow I just spent 3h debugging why the behavior of using CORS was different from using cross_origin... this is exactly the same issue. I'll try to dig a bit into the source code for a fix.

corydolphin commented 1 year ago

I was not able to reproduce this. I ran the following...

"""
Flask-Cors example
===================
This is a tiny Flask Application demonstrating Flask-Cors, making it simple
to add cross origin support to your flask app!

:copyright: (c) 2016 by Cory Dolphin.
:license:   MIT/X11, see LICENSE for more details.
"""
from flask import Flask, jsonify
import logging
try:
    # The typical way to import flask-cors
    from flask_cors import cross_origin
except ImportError:
    # Path hack allows examples to be run without installation.
    import os
    parentdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    os.sys.path.insert(0, parentdir)

    from flask_cors import cross_origin

app = Flask('FlaskCorsViewBasedExample')
logging.basicConfig(level=logging.INFO)

@app.route("/", methods=['GET'])
@cross_origin()
def helloWorld():
    '''
        This view has CORS enabled for all domains, representing the simplest
        configuration of view-based decoration. The expected result is as
        follows:

        $ curl --include -X GET http://127.0.0.1:5000/ \
            --header Origin:www.examplesite.com

        >> HTTP/1.0 200 OK
        Content-Type: text/html; charset=utf-8
        Content-Length: 184
        Access-Control-Allow-Origin: *
        Server: Werkzeug/0.9.6 Python/2.7.9
        Date: Sat, 31 Jan 2015 22:29:56 GMT

        <h1>Hello CORS!</h1> Read about my spec at the
        <a href="http://www.w3.org/TR/cors/">W3</a> Or, checkout my documentation
        on <a href="https://github.com/corydolphin/flask-cors">Github</a>

    '''
    return '''<h1>Hello CORS!</h1> Read about my spec at the
<a href="http://www.w3.org/TR/cors/">W3</a> Or, checkout my documentation
on <a href="https://github.com/corydolphin/flask-cors">Github</a>'''

@app.route("/api/v1/users/create", methods=['GET', 'POST'])
@cross_origin(allow_headers=['Content-Type'])
def cross_origin_json_post():
    '''
        This view has CORS enabled for all domains, and allows browsers
        to send the Content-Type header, allowing cross domain AJAX POST
        requests.

 Browsers will first make a preflight request to verify that the resource
        allows cross-origin POSTs with a JSON Content-Type, which can be simulated
        as:
        $ curl --include -X OPTIONS http://127.0.0.1:5000/api/v1/users/create \
            --header Access-Control-Request-Method:POST \
            --header Access-Control-Request-Headers:Content-Type \
            --header Origin:www.examplesite.com
        >> HTTP/1.0 200 OK
        Content-Type: text/html; charset=utf-8
        Allow: POST, OPTIONS
        Access-Control-Allow-Origin: *
        Access-Control-Allow-Headers: Content-Type
        Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
        Content-Length: 0
        Server: Werkzeug/0.9.6 Python/2.7.9
        Date: Sat, 31 Jan 2015 22:25:22 GMT

        $ curl --include -X POST http://127.0.0.1:5000/api/v1/users/create \
            --header Content-Type:application/json \
            --header Origin:www.examplesite.com

        >> HTTP/1.0 200 OK
        Content-Type: application/json
        Content-Length: 21
        Access-Control-Allow-Origin: *
        Server: Werkzeug/0.9.6 Python/2.7.9
        Date: Sat, 31 Jan 2015 22:25:04 GMT

        {
          "success": true
        }

    '''

    return jsonify(success=True)

@app.route("/foo", methods=['POST'])
@cross_origin()
def create_item():
      return 'bar'

@app.route("/foo", methods=['GET'])
def get_item():
      return 'foo'

if __name__ == "__main__":
    app.run(debug=True)
(flask-cors-new) ➜  flask-cors git:(switch-to-gha) ✗ curl --include -X GET http://127.0.0.1:5000/foo \
            --header Origin:www.examplesite.com
HTTP/1.1 200 OK
Server: Werkzeug/2.3.6 Python/3.11.3
Date: Mon, 26 Jun 2023 05:44:15 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3
Connection: close

foo%
(flask-cors-new) ➜  flask-cors git:(switch-to-gha) ✗ curl --include -X POST http://127.0.0.1:5000/foo \
            --header Origin:www.examplesite.com
HTTP/1.1 200 OK
Server: Werkzeug/2.3.6 Python/3.11.3
Date: Mon, 26 Jun 2023 05:44:19 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3
Access-Control-Allow-Origin: www.examplesite.com
Vary: Origin
Connection: close
pylipp commented 8 months ago

Sorry @corydolphin but this problem persists with flask-cors 4.0.0 and Flask 3.0.0.

The curl I run to mimick my browser is

curl 'http://localhost:5000/graphql' -X OPTIONS -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' -H 'Accept-Encoding: gzip, deflate, br' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type' -H 'Referer: http://localhost:5173/' -H 'Origin: http://localhost:5173' -H 'Connection: keep-alive' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: cors' -H 'Sec-Fetch-Site: same-site' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache' --verbose

I start the following flask app with flask --debug --app example.py run :

from flask import Flask, jsonify
from flask_cors import cross_origin

app = Flask(__name__)

@app.route("/graphql", methods=["GET"])
def playground():
    return jsonify("test"), 200

@app.route("/graphql", methods=["POST"])
@cross_origin(
    origins=["http://localhost:5173"],
    methods=["POST"],
    allow_headers="*",
)
def server():
    return jsonify("serving"), 200

Actual output

With the version above, the curl command returns

< HTTP/1.1 200 OK
< Server: Werkzeug/3.0.0 Python/3.10.9
< Date: Tue, 31 Oct 2023 17:57:52 GMT
< Content-Type: text/html; charset=utf-8
< Allow: POST, HEAD, GET, OPTIONS
< Content-Length: 0
< Connection: close

Expected output

When I swap the blocks such that the GET handler is last, the response correctly contains the access-control-allow-* headers:

< HTTP/1.1 200 OK
< Server: Werkzeug/3.0.0 Python/3.10.9
< Date: Tue, 31 Oct 2023 17:59:31 GMT
< Content-Type: text/html; charset=utf-8
< Allow: POST, OPTIONS, GET, HEAD
< Access-Control-Allow-Origin: http://localhost:5173
< Access-Control-Allow-Headers: content-type
< Access-Control-Allow-Methods: POST
< Content-Length: 0
< Connection: close
pylipp commented 8 months ago

I just used your example from June again. The error happens as described when the app routes are defined like below, with the non-cross_origin-decorated route first.

from flask import Flask
import logging
from flask_cors import cross_origin

app = Flask("FlaskCorsViewBasedExample")
logging.basicConfig(level=logging.INFO)

@app.route("/foo", methods=["GET"])
def get_item():
    return "foo"

@app.route("/foo", methods=["POST"])
@cross_origin()
def create_item():
    return "bar"

if __name__ == "__main__":
    app.run(debug=True)