From 311416eb5e4623a2716c2c126d183e795d821c57 Mon Sep 17 00:00:00 2001 From: Tobias Urdin Date: Mon, 11 Nov 2024 08:49:48 +0100 Subject: [PATCH 1/2] Expose redirect path in RequestRedirect When we raise the RequestRedirect exception we have already computed the new url that should be redirected to in the exception (new_url) but we don't pass the the new path that we used to compute the url. This causes another layer of redirection in depending code that needs to urlparse() the url to get the path we already have the data for. This adds new_path to the RequestRedirect exception and populates it with the path used when computing new_url. Fixes: #3000 --- CHANGES.rst | 2 ++ src/werkzeug/routing/exceptions.py | 9 ++++++--- src/werkzeug/routing/map.py | 31 +++++++++++++++++++----------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c0afdb481..8bb3e0013 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ Version 3.2.0 Unreleased +- The ``RequestRedirect`` exception now exposes ``new_path`` that + contains the request path used to compute the ``new_url``. Version 3.1.2 ------------- diff --git a/src/werkzeug/routing/exceptions.py b/src/werkzeug/routing/exceptions.py index eeabd4ed1..1cf24ceb7 100644 --- a/src/werkzeug/routing/exceptions.py +++ b/src/werkzeug/routing/exceptions.py @@ -34,9 +34,10 @@ class RequestRedirect(HTTPException, RoutingException): code = 308 - def __init__(self, new_url: str) -> None: + def __init__(self, new_url: str, new_path: t.Optional[str] = None) -> None: super().__init__(new_url) self.new_url = new_url + self.new_path = new_path def get_response( self, @@ -98,7 +99,8 @@ def _score_rule(rule: Rule) -> float: str(rule.endpoint), str(self.endpoint), ).ratio(), - 0.01 * bool(set(self.values or ()).issubset(rule.arguments)), + 0.01 * bool(set(self.values or () + ).issubset(rule.arguments)), 0.01 * bool(rule.methods and self.method in rule.methods), ] ) @@ -134,7 +136,8 @@ def __str__(self) -> str: f" Did you forget to specify values {sorted(missing_values)!r}?" ) else: - message.append(f" Did you mean {self.suggested.endpoint!r} instead?") + message.append( + f" Did you mean {self.suggested.endpoint!r} instead?") return "".join(message) diff --git a/src/werkzeug/routing/map.py b/src/werkzeug/routing/map.py index 4d15e8824..134b2da34 100644 --- a/src/werkzeug/routing/map.py +++ b/src/werkzeug/routing/map.py @@ -222,7 +222,8 @@ def bind( server_name = server_name.lower() if self.host_matching: if subdomain is not None: - raise RuntimeError("host matching enabled and a subdomain was provided") + raise RuntimeError( + "host matching enabled and a subdomain was provided") elif subdomain is None: subdomain = self.default_subdomain if script_name is None: @@ -602,12 +603,14 @@ def match( path_part = f"/{path_info.lstrip('/')}" if path_info else "" try: - result = self.map._matcher.match(domain_part, path_part, method, websocket) + result = self.map._matcher.match( + domain_part, path_part, method, websocket) except RequestPath as e: # safe = https://url.spec.whatwg.org/#url-path-segment-string new_path = quote(e.path_info, safe="!$&'()*+,/:;=@") raise RequestRedirect( - self.make_redirect_url(new_path, query_args) + self.make_redirect_url(new_path, query_args), + new_path, ) from None except RequestAliasRedirect as e: raise RequestRedirect( @@ -617,11 +620,13 @@ def match( e.matched_values, method, query_args, - ) + ), + path_part, ) from None except NoMatch as e: if e.have_match_for: - raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None + raise MethodNotAllowed( + valid_methods=list(e.have_match_for)) from None if e.websocket_mismatch: raise WebsocketMismatch() from None @@ -631,9 +636,10 @@ def match( rule, rv = result if self.map.redirect_defaults: - redirect_url = self.get_default_redirect(rule, method, rv, query_args) + (redirect_url, redirect_path) = self.get_default_redirect( + rule, method, rv, query_args) if redirect_url is not None: - raise RequestRedirect(redirect_url) + raise RequestRedirect(redirect_url, redirect_path) if rule.redirect_to is not None: if isinstance(rule.redirect_to, str): @@ -642,7 +648,8 @@ def _handle_match(match: t.Match[str]) -> str: value = rv[match.group(1)] return rule._converters[match.group(1)].to_url(value) - redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to) + redirect_url = _simple_rule_re.sub( + _handle_match, rule.redirect_to) else: redirect_url = rule.redirect_to(self, **rv) @@ -655,7 +662,8 @@ def _handle_match(match: t.Match[str]) -> str: urljoin( f"{self.url_scheme or 'http'}://{netloc}{self.script_name}", redirect_url, - ) + ), + redirect_url, ) if return_rule: @@ -736,8 +744,9 @@ def get_default_redirect( if r.provides_defaults_for(rule) and r.suitable_for(values, method): values.update(r.defaults) # type: ignore domain_part, path = r.build(values) # type: ignore - return self.make_redirect_url(path, query_args, domain_part=domain_part) - return None + return self.make_redirect_url(path, query_args, + domain_part=domain_part), path + return None, None def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str: if not isinstance(query_args, str): From 6268d54ab15bd0af7586b17bcd54cb82efef7fa5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 11 Nov 2024 08:01:05 +0000 Subject: [PATCH 2/2] [pre-commit.ci lite] apply automatic fixes --- src/werkzeug/routing/exceptions.py | 6 ++---- src/werkzeug/routing/map.py | 20 +++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/werkzeug/routing/exceptions.py b/src/werkzeug/routing/exceptions.py index 1cf24ceb7..2aaea8534 100644 --- a/src/werkzeug/routing/exceptions.py +++ b/src/werkzeug/routing/exceptions.py @@ -99,8 +99,7 @@ def _score_rule(rule: Rule) -> float: str(rule.endpoint), str(self.endpoint), ).ratio(), - 0.01 * bool(set(self.values or () - ).issubset(rule.arguments)), + 0.01 * bool(set(self.values or ()).issubset(rule.arguments)), 0.01 * bool(rule.methods and self.method in rule.methods), ] ) @@ -136,8 +135,7 @@ def __str__(self) -> str: f" Did you forget to specify values {sorted(missing_values)!r}?" ) else: - message.append( - f" Did you mean {self.suggested.endpoint!r} instead?") + message.append(f" Did you mean {self.suggested.endpoint!r} instead?") return "".join(message) diff --git a/src/werkzeug/routing/map.py b/src/werkzeug/routing/map.py index 134b2da34..10229ea76 100644 --- a/src/werkzeug/routing/map.py +++ b/src/werkzeug/routing/map.py @@ -222,8 +222,7 @@ def bind( server_name = server_name.lower() if self.host_matching: if subdomain is not None: - raise RuntimeError( - "host matching enabled and a subdomain was provided") + raise RuntimeError("host matching enabled and a subdomain was provided") elif subdomain is None: subdomain = self.default_subdomain if script_name is None: @@ -603,8 +602,7 @@ def match( path_part = f"/{path_info.lstrip('/')}" if path_info else "" try: - result = self.map._matcher.match( - domain_part, path_part, method, websocket) + result = self.map._matcher.match(domain_part, path_part, method, websocket) except RequestPath as e: # safe = https://url.spec.whatwg.org/#url-path-segment-string new_path = quote(e.path_info, safe="!$&'()*+,/:;=@") @@ -625,8 +623,7 @@ def match( ) from None except NoMatch as e: if e.have_match_for: - raise MethodNotAllowed( - valid_methods=list(e.have_match_for)) from None + raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None if e.websocket_mismatch: raise WebsocketMismatch() from None @@ -637,7 +634,8 @@ def match( if self.map.redirect_defaults: (redirect_url, redirect_path) = self.get_default_redirect( - rule, method, rv, query_args) + rule, method, rv, query_args + ) if redirect_url is not None: raise RequestRedirect(redirect_url, redirect_path) @@ -648,8 +646,7 @@ def _handle_match(match: t.Match[str]) -> str: value = rv[match.group(1)] return rule._converters[match.group(1)].to_url(value) - redirect_url = _simple_rule_re.sub( - _handle_match, rule.redirect_to) + redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to) else: redirect_url = rule.redirect_to(self, **rv) @@ -744,8 +741,9 @@ def get_default_redirect( if r.provides_defaults_for(rule) and r.suitable_for(values, method): values.update(r.defaults) # type: ignore domain_part, path = r.build(values) # type: ignore - return self.make_redirect_url(path, query_args, - domain_part=domain_part), path + return self.make_redirect_url( + path, query_args, domain_part=domain_part + ), path return None, None def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str: