Repository: pallets/werkzeug

Issue:
function `append_slash_redirect` misredirects
file `test.py`

```python
from flask import Flask, request
from werkzeug.utils import append_slash_redirect

app = Flask("test")

@app.route('/<path:path>')
def r(path):
    if not path.endswith('/'):
        return append_slash_redirect(request.environ)
    return path
```
run `FLASK_APP=test.py flask run`

A request for http://localhost:5000/parent/child gives a redirect that browsers interpret as `http://localhost:5000/parent/parent/child/`

Expecting a redirect that browsers interpret as `http://localhost:5000/parent/child/`

Possible solution:

The code strips the front '/' which leads to this situation. i.e. the returned location header is relative: 'parent/child/'
Use `environ["PATH_INFO"].rstrip("/") + "/"` instead



Discussion:
Duplicate of #1538

Resolving patch:
diff --git a/src/werkzeug/utils.py b/src/werkzeug/utils.py
--- a/src/werkzeug/utils.py
+++ b/src/werkzeug/utils.py
@@ -288,17 +288,35 @@ def redirect(
 
 
 def append_slash_redirect(environ: "WSGIEnvironment", code: int = 301) -> "Response":
-    """Redirects to the same URL but with a slash appended.  The behavior
-    of this function is undefined if the path ends with a slash already.
+    """Redirect to the current URL with a slash appended.
 
-    :param environ: the WSGI environment for the request that triggers
-                    the redirect.
+    If the current URL is ``/user/42``, the redirect URL will be
+    ``42/``. When joined to the current URL during response
+    processing or by the browser, this will produce ``/user/42/``.
+
+    The behavior is undefined if the path ends with a slash already. If
+    called unconditionally on a URL, it may produce a redirect loop.
+
+    :param environ: Use the path and query from this WSGI environment
+        to produce the redirect URL.
     :param code: the status code for the redirect.
+
+    .. versionchanged:: 2.1
+        Produce a relative URL that only modifies the last segment.
+        Relevant when the current path has multiple segments.
     """
-    new_path = environ["PATH_INFO"].strip("/") + "/"
+    tail = environ["PATH_INFO"].rpartition("/")[2]
+
+    if not tail:
+        new_path = "./"
+    else:
+        new_path = f"{tail}/"
+
     query_string = environ.get("QUERY_STRING")
+
     if query_string:
-        new_path += f"?{query_string}"
+        new_path = f"{new_path}?{query_string}"
+
     return redirect(new_path, code)
 
 


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -236,14 +236,28 @@ def test_header_set_duplication_bug():
     )
 
 
-def test_append_slash_redirect():
+@pytest.mark.parametrize(
+    "path, base_url, expected_location",
+    [
+        ("foo", "http://example.org/app", "http://example.org/app/foo/"),
+        ("/foo", "http://example.org/app", "http://example.org/app/foo/"),
+        ("/foo/bar", "http://example.org/", "http://example.org/foo/bar/"),
+        ("/foo/bar", "http://example.org/app", "http://example.org/app/foo/bar/"),
+        ("/foo?baz", "http://example.org/", "http://example.org/foo/?baz"),
+        ("/foo/", "http://example.org/", "http://example.org/foo/"),
+        ("/foo/", "http://example.org/app", "http://example.org/app/foo/"),
+        ("/", "http://example.org/", "http://example.org/"),
+        ("/", "http://example.org/app", "http://example.org/app/"),
+    ],
+)
+def test_append_slash_redirect(path, base_url, expected_location):
     def app(env, sr):
         return utils.append_slash_redirect(env)(env, sr)
 
     client = Client(app)
-    response = client.get("foo", base_url="http://example.org/app")
+    response = client.get(path, base_url=base_url)
     assert response.status_code == 301
-    assert response.headers["Location"] == "http://example.org/app/foo/"
+    assert response.headers["Location"] == expected_location
 
 
 def test_cached_property_doc():