don't intercept 307/308 routing redirects

These don't change the request body, so the debug error is no longer relevant.
This commit is contained in:
David Lord 2022-03-23 08:25:22 -07:00
parent cb7cd1e79b
commit c9a1f7ad65
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
3 changed files with 45 additions and 36 deletions

View file

@ -1457,17 +1457,26 @@ class Flask(Scaffold):
) )
def raise_routing_exception(self, request: Request) -> "te.NoReturn": def raise_routing_exception(self, request: Request) -> "te.NoReturn":
"""Exceptions that are recording during routing are reraised with """Intercept routing exceptions and possibly do something else.
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.
In debug mode, intercept a routing redirect and replace it with
an error if the body will be discarded.
With modern Werkzeug this shouldn't occur, since it now uses a
308 status which tells the browser to resend the method and
body.
.. versionchanged:: 2.1
Don't intercept 307 and 308 redirects.
:meta private:
:internal: :internal:
""" """
if ( if (
not self.debug not self.debug
or not isinstance(request.routing_exception, RequestRedirect) or not isinstance(request.routing_exception, RequestRedirect)
or request.method in ("GET", "HEAD", "OPTIONS") or request.routing_exception.code in {307, 308}
or request.method in {"GET", "HEAD", "OPTIONS"}
): ):
raise request.routing_exception # type: ignore raise request.routing_exception # type: ignore

View file

@ -41,35 +41,33 @@ class DebugFilesKeyError(KeyError, AssertionError):
class FormDataRoutingRedirect(AssertionError): class FormDataRoutingRedirect(AssertionError):
"""This exception is raised by Flask in debug mode if it detects a """This exception is raised in debug mode if a routing redirect
redirect caused by the routing system when the request method is not would cause the browser to drop the method or body. This happens
GET, HEAD or OPTIONS. Reasoning: form data will be dropped. when method is not GET, HEAD or OPTIONS and the status code is not
307 or 308.
""" """
def __init__(self, request): def __init__(self, request):
exc = request.routing_exception exc = request.routing_exception
buf = [ buf = [
f"A request was sent to this URL ({request.url}) but a" f"A request was sent to '{request.url}', but routing issued"
" redirect was issued automatically by the routing system" f" a redirect to the canonical URL '{exc.new_url}'."
f" to {exc.new_url!r}."
] ]
# In case just a slash was appended we can be extra helpful if f"{request.base_url}/" == exc.new_url.partition("?")[0]:
if f"{request.base_url}/" == exc.new_url.split("?")[0]:
buf.append( buf.append(
" The URL was defined with a trailing slash so Flask" " The URL was defined with a trailing slash. Flask"
" will automatically redirect to the URL with the" " will redirect to the URL with a trailing slash if it"
" trailing slash if it was accessed without one." " was accessed without one."
) )
buf.append( buf.append(
" Make sure to directly send your" " Send requests to the canonical URL, or use 307 or 308 for"
f" {request.method}-request to this URL since we can't make" " routing redirects. Otherwise, browsers will drop form"
" browsers or HTTP clients redirect with form data reliably" " data.\n\n"
" or without user interaction." "This exception is only raised in debug mode."
) )
buf.append("\n\nNote: this exception is only raised in debug mode") super().__init__("".join(buf))
AssertionError.__init__(self, "".join(buf).encode("utf-8"))
def attach_enctype_error_multidict(request): def attach_enctype_error_multidict(request):

View file

@ -15,6 +15,7 @@ from werkzeug.exceptions import Forbidden
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from werkzeug.http import parse_date from werkzeug.http import parse_date
from werkzeug.routing import BuildError from werkzeug.routing import BuildError
from werkzeug.routing import RequestRedirect
import flask import flask
@ -1724,28 +1725,29 @@ def test_before_first_request_functions_concurrent(app, client):
assert app.got_first_request assert app.got_first_request
def test_routing_redirect_debugging(app, client): def test_routing_redirect_debugging(monkeypatch, app, client):
app.debug = True
@app.route("/foo/", methods=["GET", "POST"]) @app.route("/foo/", methods=["GET", "POST"])
def foo(): def foo():
return "success" return "success"
with client:
with pytest.raises(AssertionError) as e:
client.post("/foo", data={})
assert "http://localhost/foo/" in str(e.value)
assert "Make sure to directly send your POST-request to this URL" in str(
e.value
)
rv = client.get("/foo", data={}, follow_redirects=True)
assert rv.data == b"success"
app.debug = False app.debug = False
rv = client.post("/foo", data={}, follow_redirects=True)
assert rv.data == b"success"
app.debug = True
with client: with client:
rv = client.post("/foo", data={}, follow_redirects=True) rv = client.post("/foo", data={}, follow_redirects=True)
assert rv.data == b"success" assert rv.data == b"success"
rv = client.get("/foo", data={}, follow_redirects=True)
assert rv.data == b"success"
monkeypatch.setattr(RequestRedirect, "code", 301)
with client, pytest.raises(AssertionError) as e:
client.post("/foo", data={})
assert "canonical URL 'http://localhost/foo/'" in str(e.value)
def test_route_decorator_custom_endpoint(app, client): def test_route_decorator_custom_endpoint(app, client):