diff --git a/flask/app.py b/flask/app.py index 009841f8..00c36a38 100644 --- a/flask/app.py +++ b/flask/app.py @@ -18,7 +18,7 @@ from itertools import chain from functools import update_wrapper from werkzeug.datastructures import ImmutableDict -from werkzeug.routing import Map, Rule +from werkzeug.routing import Map, Rule, RequestRedirect from werkzeug.exceptions import HTTPException, InternalServerError, \ MethodNotAllowed, BadRequest @@ -1134,6 +1134,22 @@ class Flask(_PackageBoundObject): return InternalServerError() return handler(e) + def raise_routing_exception(self, request): + """Exceptions that are recording during routing are reraised with + this method. During debug we are not reraising redirect requests + for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising + a different error instead to help debug situations. + + :internal: + """ + if not self.debug \ + or not isinstance(request.routing_exception, RequestRedirect) \ + or request.method in ('GET', 'HEAD', 'OPTIONS'): + raise request.routing_exception + + from .debughelpers import FormDataRoutingRedirect + raise FormDataRoutingRedirect(request) + def dispatch_request(self): """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to @@ -1146,7 +1162,7 @@ class Flask(_PackageBoundObject): """ req = _request_ctx_stack.top.request if req.routing_exception is not None: - raise req.routing_exception + self.raise_routing_exception(req) rule = req.url_rule # if we provide automatic options for this URL and the # request came with the OPTIONS method, reply automatically diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 7eae61c5..b4f73dd3 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -33,6 +33,32 @@ class DebugFilesKeyError(KeyError, AssertionError): return self.msg +class FormDataRoutingRedirect(AssertionError): + """This exception is raised by Flask in debug mode if it detects a + redirect caused by the routing system when the request method is not + GET, HEAD or OPTIONS. Reasoning: form data will be dropped. + """ + + def __init__(self, request): + exc = request.routing_exception + buf = ['A request was sent to this URL (%s) but a redirect was ' + 'issued automatically by the routing system to "%s".' + % (request.url, exc.new_url)] + + # In case just a slash was appended we can be extra helpful + if request.base_url + '/' == exc.new_url.split('?')[0]: + buf.append(' 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.') + + buf.append(' Make sure to directly send your %s-request to this URL ' + 'since we can\'t make browsers or HTTP clients redirect ' + 'with form data.' % request.method) + buf.append('\n\nNote: this exception is only raised in debug mode') + AssertionError.__init__(self, ''.join(buf).encode('utf-8')) + + def attach_enctype_error_multidict(request): """Since Flask 0.8 we're monkeypatching the files object in case a request is detected that does not use multipart form data but the files diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 7c1f4b2a..e5fbf20f 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -981,6 +981,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): self.assertEqual(got, [42]) self.assert_(app.got_first_request) + def test_routing_redirect_debugging(self): + app = flask.Flask(__name__) + app.debug = True + @app.route('/foo/', methods=['GET', 'POST']) + def foo(): + return 'success' + with app.test_client() as c: + try: + c.post('/foo', data={}) + except AssertionError, e: + self.assert_('http://localhost/foo/' in str(e)) + self.assert_('Make sure to directly send your POST-request ' + 'to this URL' in str(e)) + else: + self.fail('Expected exception') + + rv = c.get('/foo', data={}, follow_redirects=True) + self.assertEqual(rv.data, 'success') + + app.debug = False + with app.test_client() as c: + rv = c.post('/foo', data={}, follow_redirects=True) + self.assertEqual(rv.data, 'success') + class JSONTestCase(unittest.TestCase):