pallets / flask

The Python micro framework for building web applications.
https://flask.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
67.5k stars 16.14k forks source link

Handler `before_request` doesn't work as expected. #5513

Closed hrimov closed 2 months ago

hrimov commented 2 months ago

I want to implement CORS processing without any third party libraries (like flask-cors). My desire is simple: assign multiple headers to any response, access and process the pre-flight OPTIONS request.

class CORSMiddleware:
    def before_request(self):
        if request.method == "OPTIONS":
            response = jsonify({"status": "ok"})
            return self.process_response(response)

    def after_request(self, response):
        return self.process_response(response)

    # noinspection PyMethodMayBeStatic
    def process_response(self, response):
        if response:
            response.headers.add("Access-Control-Allow-Origin", "*")
            response.headers.add("Access-Control-Allow-Headers", "Content-Type, Authorization")
            response.headers.add("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE, PUT")
        return response

    def register(self, app: Flask):
        app.before_request(self.before_request)
        app.after_request(self.after_request)

It can be tested like this


# Note: helper function to reduce view logic
def add_cors_headers(response):
    response.headers.add("Access-Control-Allow-Origin", "*")
    response.headers.add("Access-Control-Allow-Headers", "Content-Type,Authorization")
    response.headers.add("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE,PUT")
    return response

user_blueprint = Blueprint("user", __name__, url_prefix="/users")

@user_blueprint.route("/signup", methods=["POST", "OPTIONS"])
def create_user_handler():
    # Note: uncomment me to see a desired behaviour
    # if request.method == "OPTIONS":
    #     response = jsonify({"status": "ok"})
    #     return add_cors_headers(response)
    # response = jsonify({"message": "registered"})
    # return add_cors_headers(response)
    return {"message": "registered"}

def create_app() -> Flask:
    app = Flask(__name__)
    app.register_blueprint(user_blueprint)
    # Note: comment me to see a desired behaviour
    CORSMiddleware().register(app)
    return app

if __name__ == "__main__":
    app = create_app()
    app.run(host="127.0.0.1", port=5000, debug=True

With the middleware, it handles a preflight OPTIONS request as an original one:

Screenshot 2024-07-01 at 10 59 27

But the desired behaviour should be like that (you can comment middleware registration and uncomment logic in the view-handler):

Screenshot 2024-07-01 at 11 00 15

Also, I was able to implement desired functionality with the middleware, but with the Werkzeug one:

from werkzeug.wrappers import Request, Response

class CORSMiddleware:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        request_ = Request(environ)

        if request_.method == "OPTIONS":
            response = Response(status=200)
            response = self.process_response(response)
            return response(environ, start_response)

        def custom_start_response(status, headers, exc_info=None):
            response_headers = self.process_headers(headers)
            return start_response(status, response_headers, exc_info)

        return self.app(environ, custom_start_response)

    def process_response(self, response):
        response.headers.add("Access-Control-Allow-Origin", "*")
        response.headers.add("Access-Control-Allow-Headers",
                             "Content-Type, Authorization")
        response.headers.add("Access-Control-Allow-Methods",
                             "GET, POST, OPTIONS, DELETE, PUT")
        return response

    def process_headers(self, headers):
        headers.append(("Access-Control-Allow-Origin", "*"))
        headers.append(("Access-Control-Allow-Headers", "Content-Type, Authorization"))
        headers.append(
            ("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE, PUT"))
        return headers

# and registering it like that
def create_app() -> Flask:
    app = Flask(__name__)
    app.register_blueprint(user_blueprint)
    app.wsgi_app = CORSMiddleware(app.wsgi_app)
    return app

So, my question is: what could be wrong with the Flask middleware?