pallets / flask

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

Registering same blueprints multiple of time with different name doesn't bring blueprint's registered error handler and before request logic with it #4124

Closed pandabear closed 3 years ago

pandabear commented 3 years ago

Prior to version Flask 2.0.2, it was possible to register blueprints multiple of times with different url prefix during Flask App factory. Such as:

    # Flask 0.10.1
    from my_project.api.v1.user import user_api_v1
    app.register_blueprint(user_api_v1,
                           url_prefix='/api/latest/users')
    app.register_blueprint(user_api_v1,
                           url_prefix='/api/v1/users')

    from my_project.api.v1.recipe import recipe_api_v1
    app.register_blueprint(recipe_api_v1,
                           url_prefix='/api/latest/recipes')
    app.register_blueprint(recipe_api_v1,
                           url_prefix='/api/v1/recipes')

But with latest Flask version, the above was updated into this:

    from my_project.api.v1.user import user_api_v1
    app.register_blueprint(user_api_v1,
                           name='users_latest',
                           url_prefix='/api/latest/users')
    app.register_blueprint(user_api_v1,
                           name='users_v1',
                           url_prefix='/api/v1/users')

    from my_project.api.v1.recipe import recipe_api_v1
    app.register_blueprint(recipe_api_v1,
                           name='recipes_latest',
                           url_prefix='/api/latest/recipes')
    app.register_blueprint(recipe_api_v1,
                           name='recipes_v1',
                           url_prefix='/api/v1/recipes')

Basically, adding unique name. However, this change caused an issue that the error- and before_request-handlers that was registered to blueprints do not get mapped to the second blueprint.

This was the result for the new Flask App:

# App's url map
Map([<Rule '/api/latest/recipes/' (GET, HEAD, OPTIONS) -> recipes_latest.get_all>,
 <Rule '/api/latest/users/' (GET, HEAD, OPTIONS) -> users_latest.get_all>,
 <Rule '/api/v1/recipes/' (GET, HEAD, OPTIONS) -> recipes_v1.get_all>,
 <Rule '/api/v1/users/' (GET, HEAD, OPTIONS) -> users_v1.get_all>,
 <Rule '/api/latest/recipes/<id>' (POST, OPTIONS) -> recipes_latest.post>,
 <Rule '/api/v1/recipes/<id>' (POST, OPTIONS) -> recipes_v1.post>,
 <Rule '/api/latest/recipes/<id>' (GET, HEAD, OPTIONS) -> recipes_latest.get>,
 <Rule '/api/v1/recipes/<id>' (GET, HEAD, OPTIONS) -> recipes_v1.get>,
 <Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>])

# App's before_request_funcs
defaultdict(<class 'list'>,
            {None: [<function requires_staff_permission at 0x7fe42b9be840>],
             'recipes_latest': [<function _require_chef_permission at 0x7fe429a67f28>],
             'users_latest': [<function _require_manager_permission at 0x7fe42acb90d0>]})

# App's error_handler_spec
defaultdict(<function Scaffold.__init__.<locals>.<lambda> at 0x7fe42ad0aa60>,
            {None: defaultdict(<class 'dict'>,
                               {None: {<class 'Exception'>: <function generic_error_handler.<locals>.error_handler at 0x7fe42ad0ab70>}}),
             'recipes_latest': defaultdict(<class 'dict'>,
                                           {None: {<class 'my_project.api.v1.recipe.MissingParameter'>: <function handle_missing_argument at 0x7fe429a68510>,
                                                   <class 'my_project.api.v1.recipe.MongoDocumentNotFound'>: <function handle_no_mongo_document at 0x7fe429a68400>,
                                                   <class 'my_project.api.v1.recipe.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe429a68488>}}),
             'users_latest': defaultdict(<class 'dict'>,
                                         {None: {<class 'my_project.api.v1.user.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe42acb92f0>}})})

What this means is that when request is done to GET /api/v1, it will not trigger any handlers, because handlers are registered to x_latest and /api/v1 was registered with x_v1 blueprint name.

The behaviour I'd expect after using app.register_blueprint for same blueprint object with different name is that the new registered name should also have same registered error- and before request-handler mapped like so:

# Expected App's before_request_funcs
defaultdict(<class 'list'>,
            {None: [<function requires_staff_permission at 0x7fe42b9be840>],
             'recipes_latest': [<function _require_chef_permission at 0x7fe429a67f28>],
             'recipes_v1': [<function _require_chef_permission at 0x7fe429a67f28>],
             'users_latest': [<function _require_manager_permission at 0x7fe42acb90d0>],
             'users_v1': [<function _require_manager_permission at 0x7fe42acb90d0>]})

# Expected App's error_handler_spec
defaultdict(<function Scaffold.__init__.<locals>.<lambda> at 0x7fe42ad0aa60>,
            {None: defaultdict(<class 'dict'>,
                               {None: {<class 'Exception'>: <function generic_error_handler.<locals>.error_handler at 0x7fe42ad0ab70>}}),
             'recipes_latest': defaultdict(<class 'dict'>,
                                           {None: {<class 'my_project.api.v1.recipe.MissingParameter'>: <function handle_missing_argument at 0x7fe429a68510>,
                                                   <class 'my_project.api.v1.recipe.MongoDocumentNotFound'>: <function handle_no_mongo_document at 0x7fe429a68400>,
                                                   <class 'my_project.api.v1.recipe.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe429a68488>}}),
             'recipes_v1': defaultdict(<class 'dict'>,
                                           {None: {<class 'my_project.api.v1.recipe.MissingParameter'>: <function handle_missing_argument at 0x7fe429a68510>,
                                                   <class 'my_project.api.v1.recipe.MongoDocumentNotFound'>: <function handle_no_mongo_document at 0x7fe429a68400>,
                                                   <class 'my_project.api.v1.recipe.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe429a68488>}}),
             'users_latest': defaultdict(<class 'dict'>,
                                         {None: {<class 'my_project.api.v1.iser.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe42acb92f0>}}),
             'users_v1': defaultdict(<class 'dict'>,
                                         {None: {<class 'my_project.api.v1.iser.UnsupportedParameter'>: <function handle_invalid_argument at 0x7fe42acb92f0>}})})

Environment:

pgjones commented 3 years ago

This makes sense, I think it is fixed with #4132