I created a blueprint with a 404 error handler. However, when I go to non-existent urls under the blueprint's prefix, the standard 404 page is shown rather than my custom one. How can I make the blueprint handle 404 errors correctly?
The following is a short app that demonstrates the problem. Navigating to http://localhost:5000/simple/asdf will not show the blueprint's error page.
#!/usr/local/bin/python
# coding: utf-8
from flask import *
from config import PORT, HOST, DEBUG
simplepage = Blueprint('simple', __name__, url_prefix='/simple')
#simplepage.route('/')
def simple_root():
return 'This simple page'
#simplepage.errorhandler(404)
def error_simple(err):
return 'This simple error 404', err
app = Flask(__name__)
app.config.from_pyfile('config.py')
app.register_blueprint(simplepage)
#app.route('/', methods=['GET'])
def api_get():
return render_template('index.html')
if __name__ == '__main__':
app.run(host=HOST,
port=PORT,
debug=DEBUG)
The documentation mentions that 404 error handlers will not behave as expected on blueprints. The app handles routing and raises a 404 before the request gets to the blueprint. The 404 handler will still activate for abort(404) because that is happening after routing at the blueprint level.
This is something that could possibly be fixed in Flask (there's an open issue about it). As a workaround, you can do your own error routing within the top-level 404 handler.
from flask import request, render_template
#app.errorhandler(404)
def handle_404(e):
path = request.path
# go through each blueprint to find the prefix that matches the path
# can't use request.blueprint since the routing didn't match anything
for bp_name, bp in app.blueprints.items():
if path.startswith(bp.url_prefix):
# get the 404 handler registered by the blueprint
handler = app.error_handler_spec.get(bp_name, {}).get(404)
if handler is not None:
# if a handler was found, return it's response
return handler(e)
# return a default response
return render_template('404.html'), 404
Related
There is a blueprint with a lot of useful routes defined, but I have no control over it (can not change it's code in any way)
Trying to reuse it in a different app but one of the blueprint's endpoints must be overloaded. How can I achieve that?
I tried just adding a new route to blueprint on top of the existing one:
#blueprint.route('/my/route', methods=['PUT', 'POST'])
def my_new_view_func(program, project):
# some new behavior for the endpoint
As the result there is duplicate url_rule in app.url_map.iter_rules():
<Rule '/my/route' (PUT, POST) -> my_view_func>,
<Rule '/my/route' (PUT, POST) -> my_new_view_func>,
and when requesting /my/route old viewer my_view_func gets executed
Can I somehow get rid of the old url rule? Or maybe there is a better way to overwrite the route?
There are 2 solutions which I found. First:
from flask import Flask, Blueprint
simple_page = Blueprint('simple_page', __name__, )
#simple_page.route('/my/route/')
def my():
# for example it's a registered route somewhere...
return 'default'
#simple_page.route('/my/route/')
def new_my():
# new endpoint / should works instead my()
return 'new'
# map of views which we won't register in Flask app
# you can store this somewhere in settings
SKIP_VIEWS = (
# route, view function
('/my/route/', my, ),
)
class CustomFlask(Flask):
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
# Flask registers views when an application starts
# do not add view from SKIP_VIEWS
for rule_, view_func_ in SKIP_VIEWS: # type: str, func
if rule_ == rule and view_func == view_func_:
return
return super(CustomFlask, self).add_url_rule(rule, endpoint, view_func, **options)
app = CustomFlask(__name__)
app.register_blueprint(simple_page)
app.run(debug=True)
Second way:
two.py - default blueprint with endpoint
from flask import Blueprint
bp_two = Blueprint('simple_page2', __name__, )
#bp_two.route('/my/route/')
def default():
return 'default'
test.py - your blueprint + app
from flask import Flask, Blueprint
from two import bp_two
your_bp = Blueprint('simple_page', __name__, )
#your_bp.route('/my/route/')
def new_route():
return 'new'
app = Flask(__name__)
# register blueprint and turn off '/my/route/' endpoint
app.register_blueprint(bp_two, **{'url_defaults': {'/my/route/': None}})
app.register_blueprint(your_bp)
app.run(debug=True)
Run app. Open /my/route/. You will see that default endpoint wasn't add/works.
Hope this helps.
I am building a Flask app with a blueprint mounted on two different endpoint (one is a legacy alias to the other).
In my blueprint class:
ldp = Blueprint('ldp', __name__)
#ldp.route('/<path:uuid>', methods=['GET'])
#ldp.route('/', defaults={'uuid': None}, methods=['GET'],
strict_slashes=False)
def get_resource(uuid):
# Route code...
In my main server code:
app = Flask(__name__)
app.config.update(config['flask'])
app.register_blueprint(ldp, url_prefix='/new_ep')
# Legacy endpoint. #TODO Deprecate.
app.register_blueprint(ldp, url_prefix='/old_ep')
How can I get the actual URL of the request up to the /old_ep or /new_ep part in the route method, e.g. http://localhost:5000/new_ep?
So far I have used
request.host_url + request.path.split('/')[1]
but it looks quite inelegant and possibly error-prone. I would like to use the information from the blueprint setup if possible.
Thanks for your help.
EDIT: I could get to the Blueprint instance from within the request with
current_app.blueprints[request.blueprint]
and I was hoping that the url_prefix attribute that I set when registering the blueprint was there, but it is None instead. As I read from the documentation for the supposedly related iter_blueprints() method, apparently these blueprints are listed without regard of how many times and with which parameters they were registered. Too bad.
Here is a full working example to get the idea based off issue 612
from flask import Flask, Blueprint, url_for, request, g
bp = Blueprint('whatever', __name__)
#bp.url_defaults
def bp_url_defaults(endpoint, values):
url_prefix = getattr(g, 'url_prefix', None)
if url_prefix is not None:
values.setdefault('url_prefix', url_prefix)
#bp.url_value_preprocessor
def bp_url_value_preprocessor(endpoint, values):
g.url_prefix = values.pop('url_prefix')
#bp.route('/something')
def index():
return 'host prefix is %s%s' % (request.host_url, g.url_prefix)
app = Flask(__name__)
app.register_blueprint(bp, url_prefix='/new_ep', url_defaults={'url_prefix': 'new_ep'})
app.register_blueprint(bp, url_prefix='/old_ep', url_defaults={'url_prefix': 'old_ep'})
The documentation states that the preferred way to define a route is to include a trailing slash:
#app.route('/foo/', methods=['GET'])
def get_foo():
pass
This way, a client can GET /foo or GET /foo/ and receive the same result.
However, POSTed methods do not have the same behavior.
from flask import Flask
app = Flask(__name__)
#app.route('/foo/', methods=['POST'])
def post_foo():
return "bar"
app.run(port=5000)
Here, if you POST /foo, it will fail with method not allowed if you are not running in debug mode, or it will fail with the following notice if you are in debug mode:
A request was sent to this URL (http://localhost:5000/foo) but a redirect was issued automatically by the routing system to "http://localhost:5000/foo/". The URL was defined with a trailing slash so Flask will automatically redirect to the URL with the trailing slash if it was accessed without one. Make sure to directly send your POST-request to this URL since we can't make browsers or HTTP clients redirect with form data reliably or without user interaction
Moreover, it appears that you cannot even do this:
#app.route('/foo', methods=['POST'])
#app.route('/foo/', methods=['POST'])
def post_foo():
return "bar"
Or this:
#app.route('/foo', methods=['POST'])
def post_foo_no_slash():
return redirect(url_for('post_foo'), code=302)
#app.route('/foo/', methods=['POST'])
def post_foo():
return "bar"
Is there any way to get POST to work on both non-trailing and trailing slashes?
Please refer to this post:
Trailing slash triggers 404 in Flask path rule
You can disable strict slashes to support your needs
Globally:
app = Flask(__name__)
app.url_map.strict_slashes = False
... or per route
#app.route('/foo', methods=['POST'], strict_slashes=False)
def foo():
return 'foo'
You can also check this link. There is separate discussion on github on this one. https://github.com/pallets/flask/issues/1783
You can check request.path whether /foo/ or not then redirect it to where you want:
#app.before_request
def before_request():
if request.path == '/foo':
return redirect(url_for('foo'), code=123)
#app.route('/foo/', methods=['POST'])
def foo():
return 'foo'
$ http post localhost:5000/foo
127.0.0.1 - - [08/Mar/2017 13:06:48] "POST /foo HTTP/1.1" 123
I'm building the Flask app with React, I ended up having a problem with routing.
The backend is responsible to be an API, hence some routes look like:
#app.route('/api/v1/do-something/', methods=["GET"])
def do_something():
return something()
and the main route which leads to the React:
#app.route('/')
def index():
return render_template('index.html')
I'm using react-router in the React app, everything works fine, react-router takes me to /something and I get the rendered view, but when I refresh the page on /something then Flask app takes care of this call and I get Not Found error.
What is the best solution? I was thinking about redirecting all calls which are not calling /api/v1/... to / it's not ideal as I will get back the home page of my app, not rendered React view.
We used catch-all URLs for this.
from flask import Flask
app = Flask(__name__)
#app.route('/', defaults={'path': ''})
#app.route('/<path:path>')
def catch_all(path):
return 'You want path: %s' % path
if __name__ == '__main__':
app.run()
You can also go an extra mile and reuse the Flask routing system to match path to the same routes as client so you can embed the data client will need as JSON inside the HTML response.
Maybe as extension to the answers before. This solved the problem for me:
from flask import send_from_directory
#app.route('/', defaults={'path': ''})
#app.route('/<path:path>')
def serve(path):
path_dir = os.path.abspath("../build") #path react build
if path != "" and os.path.exists(os.path.join(path_dir, path)):
return send_from_directory(os.path.join(path_dir), path)
else:
return send_from_directory(os.path.join(path_dir),'index.html')
For some reason, the catch-all URLs did not work for me. I found that using the flask 404 handler results in the exact same thing. It sees the url and passes it down to react where your router will handle it.
#app.errorhandler(404)
def not_found(e):
return app.send_static_file('index.html')
Just to inform handle error 404 and render_template works perfectly for me.
#app.errorhandler(404)
def not_found(e):
return render_template("index.html")
I have to combine both catch-all and 404 handler for it to work properly. I am hosting a react-app in a subpath with its own redirection handler from react-router.
#app.route('/sub-path', defaults={'path': 'index.html'})
#app.route('/sub-path/<path:path>')
def index(path):
return send_from_directory('../react-dir/build', path)
#app.errorhandler(404)
def not_found(e):
return send_from_directory('../react-dir/build','index.html')
I'm attempting to test a custom error page in flask (404 in this case).
I've defined my custom 404 page as such:
#app.errorhandler(404)
def page_not_found(e):
print "Custom 404!"
return render_template('404.html'), 404
This works perfectly when hitting an unknown page in a browser (I see Custom 404! in stdout and my custom content is visible). However, when trying to trigger a 404 via unittest with nose, the standard/server 404 page renders. I get no log message or the custom content I am trying to test for.
My test case is defined like so:
class MyTestCase(TestCase):
def setUp(self):
self.app = create_app()
self.app_context = self.app.app_context()
self.app.config.from_object('config.TestConfiguration')
self.app.debug = False # being explicit to debug what's going on...
self.app_context.push()
self.client = self.app.test_client()
def tearDown(self):
self.app_context.pop()
def test_custom_404(self):
path = '/non_existent_endpoint'
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
self.assertIn(path, response.data)
I have app.debug explicitly set to False on my test app. Is there something else I have to explicitly set?
After revisiting this with fresh eyes, it's obvious that the problem is in my initialization of the application and not in my test/configuration. My app's __init__.py basically looks like this:
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
return app
app = create_app()
# Custom error pages
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
Notice that the error handler is attached to #app outside of create_app(), the method I'm calling in my TestCase.setUp() method.
If I simply move that error handler up into the create_app() method, everything works fine... but it feels a bit gross? Maybe?
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
# Custom error pages
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
return app
This ultimately answers my question and fixes my problem, but I'd love other thoughts on how to differently register those errorhandlers.
The Flask application object has an error_handler_spec property that can be mocked to solve this:
A dictionary of all registered error handlers. The key is None for
error handlers active on the application, otherwise the key is the
name of the blueprint. Each key points to another dictionary where the
key is the status code of the http exception. The special key None
points to a list of tuples where the first item is the class for the
instance check and the second the error handler function.
So something like this in your test method should work:
mock_page_not_found = mock.magicMock()
mock_page_not_found.return_value = {}, 404
with mock.patch.dict(self.app.error_handler_spec[None], {404: mock_page_not_found}):
path = '/non_existent_endpoint'
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
mock_page_not_found.assert_called_once()
In reference to the following comment you made "If I simply move that error handler up into the create_app() method, everything works fine... but it feels a bit gross? Maybe?":
You can define a function to register an error handler and call that in your create_app function:
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
register_error_pages(app)
return app
app = create_app()
# Custom error pages
def register_error_pages(app):
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
That way if you have more custom error handlers you want to register (403, 405, 500) you can define them inside the register_error_pages function instead of your create_app function.