Repository: encode/starlette

Issue:
RedirectResponse call to `quote_plus` replaces spaces & `%20`'s with `+` character
### Checklist

<!-- Please make sure you check all these items before submitting your bug report. -->

- [x] The bug is reproducible against the latest release and/or `master`.
- [x] There are no similar issues or pull requests to fix it yet.

### Describe the bug

<!-- A clear and concise description of what the bug is. -->

`RedirectResponse` replaces url paths with spaces or `%20` with the `+` character. This happens as the `location` header is written after passing the url to `quote_plus`: https://github.com/encode/starlette/blob/master/starlette/responses.py#L181

If part of the path was a path param (with a space in it), this then modified those path params.

We currently encounter this with trailing slash redirects, which return a RedirectResponse.

### To reproduce

<!-- Provide a *minimal* example with steps to reproduce the bug locally.

NOTE: try to keep any external dependencies *at an absolute minimum*
(middleware, servers, proxies, certificates...).
In other words, remove anything that doesn't make the bug go away.
-->

```python
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route


async def homepage(request):
    name = request.path_params["name"]
    return JSONResponse({"hello": name})


app = Starlette(routes=[Route("/{name}/greeting", homepage)])


from starlette.testclient import TestClient

client = TestClient(app)


def test_redirect_homepage():
    name = "bob%20ross"
    url = f"{name}/greeting"
    for u in [url, url + "/"]:
        resp = client.get(u)
        assert url in str(resp.url)
```

### Expected behavior

<!-- A clear and concise description of what you expected to happen. -->

Expect that the location header would contain the url to redirect to without modification. If needed, it could replace the actual space character with `%20`, instead, but it's not clear to me that is necessary, after looking through the http specs.

This could be accomplished with swapping `urllib.quote_plus` for `urllib.quote` in `RedirectResponse`.

### Actual behavior

<!-- A clear and concise description of what actually happens. -->

Spaces are being replaced with the `+` character. 

<!-- Any tracebacks, screenshots, etc. that can help understanding the problem.

NOTE:
- Please list tracebacks in full (don't truncate them).
- Consider using `<details>` to make tracebacks/logs collapsible if they're very large (see https://gist.github.com/ericclemmons/b146fe5da72ca1f706b2ef72a20ac39d).
-->

### Environment

- OS: <!-- eg Linux/Windows/macOS. --> macOS
- Python version: <!-- eg 3.8.5 (get it with `$ python -V`). --> 3.8.8
- Starlette version: <!-- eg 0.13.8 (get it with `$ pip show starlette`). --> 0.13.6 (also separately tested 0.14.2)

Discussion:

Patch:
diff --git a/starlette/responses.py b/starlette/responses.py
index ff122fb..d660cd9 100644
--- a/starlette/responses.py
+++ b/starlette/responses.py
@@ -7,7 +7,7 @@ import sys
 import typing
 from email.utils import formatdate
 from mimetypes import guess_type as mimetypes_guess_type
-from urllib.parse import quote, quote_plus
+from urllib.parse import quote
 
 from starlette.background import BackgroundTask
 from starlette.concurrency import iterate_in_threadpool, run_until_first_complete
@@ -178,7 +178,7 @@ class RedirectResponse(Response):
         super().__init__(
             content=b"", status_code=status_code, headers=headers, background=background
         )
-        self.headers["location"] = quote_plus(str(url), safe=":/%#?&=@[]!$&'()*+,;")
+        self.headers["location"] = quote(str(url), safe=":/%#?=@[]!$&'()*+,;")
 
 
 class StreamingResponse(Response):


Test patch:
diff --git a/tests/test_responses.py b/tests/test_responses.py
index 10fbe67..fd2ba0e 100644
--- a/tests/test_responses.py
+++ b/tests/test_responses.py
@@ -60,6 +60,20 @@ def test_redirect_response():
     assert response.url == "http://testserver/"
 
 
+def test_quoting_redirect_response():
+    async def app(scope, receive, send):
+        if scope["path"] == "/I ♥ Starlette/":
+            response = Response("hello, world", media_type="text/plain")
+        else:
+            response = RedirectResponse("/I ♥ Starlette/")
+        await response(scope, receive, send)
+
+    client = TestClient(app)
+    response = client.get("/redirect")
+    assert response.text == "hello, world"
+    assert response.url == "http://testserver/I%20%E2%99%A5%20Starlette/"
+
+
 def test_streaming_response():
     filled_by_bg_task = ""