Repository: encode/starlette

Issue:
Form field names do not escape perfect encoding
I switched one microservice from Flask to Starlette and got unexpected form parsing problem.
This code will return %20 in places where spaces were supposed to be:
```
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.requests import Request as StarletteRequest

SERVER = Starlette()

@SERVER.route("/send", methods=["POST"])
async def send_endpoint(request: StarletteRequest) -> PlainTextResponse:
    form = await request.form()
    if "number" in form and "name" in form:
        number = form["number"]
        name = form["name"]
        return PlainTextResponse("Number: " + number + " ; Name: " + name)
```

Maybe there are other issues with form parsing (x-www-form-urlencoded).


Discussion:
Hmm, this seems to be an issue with the `FormParser` class. I think this is easily fixed if we call `urllib.parse.unquote` on the decoded field value.

I'll submit a PR fixing this
Nice work - problem is it doesn't unquote the field names, e.g. field[0] remains field%5B0%5D

I'm pretty sure the multipart library can already handle all of this correctly if you pass it the Content-Type header...
> I'm pretty sure the multipart library can already handle all of this correctly if you pass it the Content-Type header...

Perhaps - I don't know. QuerystringParser doesn't appear to care about the content-type header (and it's not obvious to me why the header would make a difference in this case).

https://andrew-d.github.io/python-multipart/api.html#multipart.multipart.QuerystringParser
Sorry, I was referring to the main FormParser rather than the QuerystringParser in multipart.
After all, forms can be POSTed as application/x-www-form-urlencoded or multipart/form-data.
Okay, does this need reopening then?
Anyone fancy submitting either a failing test case, or a test case and fix?
Retitled to reflect the one remaining issue here.

Resolving patch:
diff --git a/starlette/formparsers.py b/starlette/formparsers.py
--- a/starlette/formparsers.py
+++ b/starlette/formparsers.py
@@ -125,9 +125,9 @@ async def parse(self) -> typing.Dict[str, typing.Union[str, UploadFile]]:
                 elif message_type == FormMessage.FIELD_DATA:
                     field_value += message_bytes
                 elif message_type == FormMessage.FIELD_END:
-                    result[field_name.decode("latin-1")] = unquote_plus(
-                        field_value.decode("latin-1")
-                    )
+                    name = unquote_plus(field_name.decode("latin-1"))
+                    value = unquote_plus(field_value.decode("latin-1"))
+                    result[name] = value
                 elif message_type == FormMessage.END:
                     pass
 


Test patch:
diff --git a/tests/test_formparsers.py b/tests/test_formparsers.py
--- a/tests/test_formparsers.py
+++ b/tests/test_formparsers.py
@@ -129,6 +129,18 @@ def test_no_request_data(tmpdir):
     assert response.json() == {}
 
 
+def test_urlencoded_percent_encoding(tmpdir):
+    client = TestClient(app)
+    response = client.post("/", data={"some": "da ta"})
+    assert response.json() == {"some": "da ta"}
+
+
+def test_urlencoded_percent_encoding_keys(tmpdir):
+    client = TestClient(app)
+    response = client.post("/", data={"so me": "data"})
+    assert response.json() == {"so me": "data"}
+
+
 def test_urlencoded_multi_field_app_reads_body(tmpdir):
     client = TestClient(app_read_body)
     response = client.post("/", data={"some": "data", "second": "key pair"})