We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
non-ASCII characters filename in multipart/form-data
I tried to send a file with russian letters in the file name, and `httpx` escaped all non-ASCII characters (as I understood it the way specified in [RFC 7578](https://tools.ietf.org/html/rfc7578)). But this is different from `requests` [behavior](https://github.com/psf/requests/blob/589a82256759018a7e5e289302898dae32544949/requests/models.py#L110), where the file name is simply written as is (using [`RequestField.make_multipart`](https://github.com/urllib3/urllib3/blob/f0d9ebc41e51c4c4c9990b1eed02d297fd1b20d8/src/urllib3/fields.py#L248) from `urllib3`). Maybe `httpx` should just change [`FileField.render_headers`](https://github.com/encode/httpx/blob/3ba2e8c328f05b07ee06ae40f99dce7fe1e8292c/httpx/multipart.py#L57), so as not to escape the file name and instead just put it as bytes?

```diff
def render_headers(self) -> bytes:
-    name = quote(self.name, encoding="utf-8").encode("ascii")
-    filename = quote(self.filename, encoding="utf-8").encode("ascii")
-    content_type = self.content_type.encode("ascii")
     return b"".join(
         [
             b'Content-Disposition: form-data; name="',
-            name,
+            self.name.encode(),
             b'"; filename="',
-            filename,
+            self.filename.encode(),
             b'"\r\n',
             b"Content-Type: ",
-            content_type,
+            self.content_type.encode(),
             b"\r\n",
        ]
    )
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `httpx/multipart.py`
Content:
```
1 import binascii
2 import mimetypes
3 import os
4 import typing
5 from io import BytesIO
6 from urllib.parse import quote
7 
8 
9 class Field:
10     def render_headers(self) -> bytes:
11         raise NotImplementedError()  # pragma: nocover
12 
13     def render_data(self) -> bytes:
14         raise NotImplementedError()  # pragma: nocover
15 
16 
17 class DataField(Field):
18     def __init__(self, name: str, value: typing.Union[str, bytes]) -> None:
19         if not isinstance(name, str):
20             raise TypeError("Invalid type for name. Expected str.")
21         if not isinstance(value, (str, bytes)):
22             raise TypeError("Invalid type for value. Expected str or bytes.")
23         self.name = name
24         self.value = value
25 
26     def render_headers(self) -> bytes:
27         name = quote(self.name, encoding="utf-8").encode("ascii")
28         return b"".join(
29             [b'Content-Disposition: form-data; name="', name, b'"\r\n' b"\r\n"]
30         )
31 
32     def render_data(self) -> bytes:
33         return (
34             self.value if isinstance(self.value, bytes) else self.value.encode("utf-8")
35         )
36 
37 
38 class FileField(Field):
39     def __init__(
40         self, name: str, value: typing.Union[typing.IO[typing.AnyStr], tuple]
41     ) -> None:
42         self.name = name
43         if not isinstance(value, tuple):
44             self.filename = os.path.basename(getattr(value, "name", "upload"))
45             self.file = value  # type: typing.Union[typing.IO[str], typing.IO[bytes]]
46             self.content_type = self.guess_content_type()
47         else:
48             self.filename = value[0]
49             self.file = value[1]
50             self.content_type = (
51                 value[2] if len(value) > 2 else self.guess_content_type()
52             )
53 
54     def guess_content_type(self) -> str:
55         return mimetypes.guess_type(self.filename)[0] or "application/octet-stream"
56 
57     def render_headers(self) -> bytes:
58         name = quote(self.name, encoding="utf-8").encode("ascii")
59         filename = quote(self.filename, encoding="utf-8").encode("ascii")
60         content_type = self.content_type.encode("ascii")
61         return b"".join(
62             [
63                 b'Content-Disposition: form-data; name="',
64                 name,
65                 b'"; filename="',
66                 filename,
67                 b'"\r\n',
68                 b"Content-Type: ",
69                 content_type,
70                 b"\r\n",
71                 b"\r\n",
72             ]
73         )
74 
75     def render_data(self) -> bytes:
76         content = self.file.read()
77         return content.encode("utf-8") if isinstance(content, str) else content
78 
79 
80 def iter_fields(data: dict, files: dict) -> typing.Iterator[Field]:
81     for name, value in data.items():
82         if isinstance(value, (list, dict)):
83             for item in value:
84                 yield DataField(name=name, value=item)
85         else:
86             yield DataField(name=name, value=value)
87 
88     for name, value in files.items():
89         yield FileField(name=name, value=value)
90 
91 
92 def multipart_encode(data: dict, files: dict) -> typing.Tuple[bytes, str]:
93     body = BytesIO()
94     boundary = binascii.hexlify(os.urandom(16))
95 
96     for field in iter_fields(data, files):
97         body.write(b"--%s\r\n" % boundary)
98         body.write(field.render_headers())
99         body.write(field.render_data())
100         body.write(b"\r\n")
101 
102     body.write(b"--%s--\r\n" % boundary)
103 
104     content_type = "multipart/form-data; boundary=%s" % boundary.decode("ascii")
105 
106     return body.getvalue(), content_type
107
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/httpx/multipart.py b/httpx/multipart.py
--- a/httpx/multipart.py
+++ b/httpx/multipart.py
@@ -1,9 +1,17 @@
 import binascii
 import mimetypes
 import os
+import re
 import typing
 from io import BytesIO
-from urllib.parse import quote
+
+_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
+_HTML5_FORM_ENCODING_REPLACEMENTS.update(
+    {chr(c): "%{:02X}".format(c) for c in range(0x00, 0x1F + 1) if c != 0x1B}
+)
+_HTML5_FORM_ENCODING_RE = re.compile(
+    r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])
+)
 
 
 class Field:
@@ -24,10 +32,8 @@
         self.value = value
 
     def render_headers(self) -> bytes:
-        name = quote(self.name, encoding="utf-8").encode("ascii")
-        return b"".join(
-            [b'Content-Disposition: form-data; name="', name, b'"\r\n' b"\r\n"]
-        )
+        name = _format_param("name", self.name)
+        return b"".join([b"Content-Disposition: form-data; ", name, b"\r\n\r\n"])
 
     def render_data(self) -> bytes:
         return (
@@ -55,20 +61,18 @@
         return mimetypes.guess_type(self.filename)[0] or "application/octet-stream"
 
     def render_headers(self) -> bytes:
-        name = quote(self.name, encoding="utf-8").encode("ascii")
-        filename = quote(self.filename, encoding="utf-8").encode("ascii")
-        content_type = self.content_type.encode("ascii")
+        name = _format_param("name", self.name)
+        filename = _format_param("filename", self.filename)
+        content_type = self.content_type.encode()
         return b"".join(
             [
-                b'Content-Disposition: form-data; name="',
+                b"Content-Disposition: form-data; ",
                 name,
-                b'"; filename="',
+                b"; ",
                 filename,
-                b'"\r\n',
-                b"Content-Type: ",
+                b"\r\nContent-Type: ",
                 content_type,
-                b"\r\n",
-                b"\r\n",
+                b"\r\n\r\n",
             ]
         )
 
@@ -104,3 +108,14 @@
     content_type = "multipart/form-data; boundary=%s" % boundary.decode("ascii")
 
     return body.getvalue(), content_type
+
+
+def _format_param(name: str, value: typing.Union[str, bytes]) -> bytes:
+    if isinstance(value, bytes):
+        value = value.decode()
+        
+    def replacer(match: typing.Match[str]) -> str:
+        return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
+
+    value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
+    return f'{name}="{value}"'.encode()

{"golden_diff": "diff --git a/httpx/multipart.py b/httpx/multipart.py\n--- a/httpx/multipart.py\n+++ b/httpx/multipart.py\n@@ -1,9 +1,17 @@\n import binascii\n import mimetypes\n import os\n+import re\n import typing\n from io import BytesIO\n-from urllib.parse import quote\n+\n+_HTML5_FORM_ENCODING_REPLACEMENTS = {'\"': \"%22\", \"\\\\\": \"\\\\\\\\\"}\n+_HTML5_FORM_ENCODING_REPLACEMENTS.update(\n+    {chr(c): \"%{:02X}\".format(c) for c in range(0x00, 0x1F + 1) if c != 0x1B}\n+)\n+_HTML5_FORM_ENCODING_RE = re.compile(\n+    r\"|\".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])\n+)\n \n \n class Field:\n@@ -24,10 +32,8 @@\n         self.value = value\n \n     def render_headers(self) -> bytes:\n-        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n-        return b\"\".join(\n-            [b'Content-Disposition: form-data; name=\"', name, b'\"\\r\\n' b\"\\r\\n\"]\n-        )\n+        name = _format_param(\"name\", self.name)\n+        return b\"\".join([b\"Content-Disposition: form-data; \", name, b\"\\r\\n\\r\\n\"])\n \n     def render_data(self) -> bytes:\n         return (\n@@ -55,20 +61,18 @@\n         return mimetypes.guess_type(self.filename)[0] or \"application/octet-stream\"\n \n     def render_headers(self) -> bytes:\n-        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n-        filename = quote(self.filename, encoding=\"utf-8\").encode(\"ascii\")\n-        content_type = self.content_type.encode(\"ascii\")\n+        name = _format_param(\"name\", self.name)\n+        filename = _format_param(\"filename\", self.filename)\n+        content_type = self.content_type.encode()\n         return b\"\".join(\n             [\n-                b'Content-Disposition: form-data; name=\"',\n+                b\"Content-Disposition: form-data; \",\n                 name,\n-                b'\"; filename=\"',\n+                b\"; \",\n                 filename,\n-                b'\"\\r\\n',\n-                b\"Content-Type: \",\n+                b\"\\r\\nContent-Type: \",\n                 content_type,\n-                b\"\\r\\n\",\n-                b\"\\r\\n\",\n+                b\"\\r\\n\\r\\n\",\n             ]\n         )\n \n@@ -104,3 +108,14 @@\n     content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\"ascii\")\n \n     return body.getvalue(), content_type\n+\n+\n+def _format_param(name: str, value: typing.Union[str, bytes]) -> bytes:\n+    if isinstance(value, bytes):\n+        value = value.decode()\n+        \n+    def replacer(match: typing.Match[str]) -> str:\n+        return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]\n+\n+    value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)\n+    return f'{name}=\"{value}\"'.encode()\n", "issue": "non-ASCII characters filename in multipart/form-data\nI tried to send a file with russian letters in the file name, and `httpx` escaped all non-ASCII characters (as I understood it the way specified in [RFC 7578](https://tools.ietf.org/html/rfc7578)). But this is different from `requests` [behavior](https://github.com/psf/requests/blob/589a82256759018a7e5e289302898dae32544949/requests/models.py#L110), where the file name is simply written as is (using [`RequestField.make_multipart`](https://github.com/urllib3/urllib3/blob/f0d9ebc41e51c4c4c9990b1eed02d297fd1b20d8/src/urllib3/fields.py#L248) from `urllib3`). Maybe `httpx` should just change [`FileField.render_headers`](https://github.com/encode/httpx/blob/3ba2e8c328f05b07ee06ae40f99dce7fe1e8292c/httpx/multipart.py#L57), so as not to escape the file name and instead just put it as bytes?\r\n\r\n```diff\r\ndef render_headers(self) -> bytes:\r\n-    name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\r\n-    filename = quote(self.filename, encoding=\"utf-8\").encode(\"ascii\")\r\n-    content_type = self.content_type.encode(\"ascii\")\r\n     return b\"\".join(\r\n         [\r\n             b'Content-Disposition: form-data; name=\"',\r\n-            name,\r\n+            self.name.encode(),\r\n             b'\"; filename=\"',\r\n-            filename,\r\n+            self.filename.encode(),\r\n             b'\"\\r\\n',\r\n             b\"Content-Type: \",\r\n-            content_type,\r\n+            self.content_type.encode(),\r\n             b\"\\r\\n\",\r\n        ]\r\n    )\r\n```\n", "before_files": [{"content": "import binascii\nimport mimetypes\nimport os\nimport typing\nfrom io import BytesIO\nfrom urllib.parse import quote\n\n\nclass Field:\n    def render_headers(self) -> bytes:\n        raise NotImplementedError()  # pragma: nocover\n\n    def render_data(self) -> bytes:\n        raise NotImplementedError()  # pragma: nocover\n\n\nclass DataField(Field):\n    def __init__(self, name: str, value: typing.Union[str, bytes]) -> None:\n        if not isinstance(name, str):\n            raise TypeError(\"Invalid type for name. Expected str.\")\n        if not isinstance(value, (str, bytes)):\n            raise TypeError(\"Invalid type for value. Expected str or bytes.\")\n        self.name = name\n        self.value = value\n\n    def render_headers(self) -> bytes:\n        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n        return b\"\".join(\n            [b'Content-Disposition: form-data; name=\"', name, b'\"\\r\\n' b\"\\r\\n\"]\n        )\n\n    def render_data(self) -> bytes:\n        return (\n            self.value if isinstance(self.value, bytes) else self.value.encode(\"utf-8\")\n        )\n\n\nclass FileField(Field):\n    def __init__(\n        self, name: str, value: typing.Union[typing.IO[typing.AnyStr], tuple]\n    ) -> None:\n        self.name = name\n        if not isinstance(value, tuple):\n            self.filename = os.path.basename(getattr(value, \"name\", \"upload\"))\n            self.file = value  # type: typing.Union[typing.IO[str], typing.IO[bytes]]\n            self.content_type = self.guess_content_type()\n        else:\n            self.filename = value[0]\n            self.file = value[1]\n            self.content_type = (\n                value[2] if len(value) > 2 else self.guess_content_type()\n            )\n\n    def guess_content_type(self) -> str:\n        return mimetypes.guess_type(self.filename)[0] or \"application/octet-stream\"\n\n    def render_headers(self) -> bytes:\n        name = quote(self.name, encoding=\"utf-8\").encode(\"ascii\")\n        filename = quote(self.filename, encoding=\"utf-8\").encode(\"ascii\")\n        content_type = self.content_type.encode(\"ascii\")\n        return b\"\".join(\n            [\n                b'Content-Disposition: form-data; name=\"',\n                name,\n                b'\"; filename=\"',\n                filename,\n                b'\"\\r\\n',\n                b\"Content-Type: \",\n                content_type,\n                b\"\\r\\n\",\n                b\"\\r\\n\",\n            ]\n        )\n\n    def render_data(self) -> bytes:\n        content = self.file.read()\n        return content.encode(\"utf-8\") if isinstance(content, str) else content\n\n\ndef iter_fields(data: dict, files: dict) -> typing.Iterator[Field]:\n    for name, value in data.items():\n        if isinstance(value, (list, dict)):\n            for item in value:\n                yield DataField(name=name, value=item)\n        else:\n            yield DataField(name=name, value=value)\n\n    for name, value in files.items():\n        yield FileField(name=name, value=value)\n\n\ndef multipart_encode(data: dict, files: dict) -> typing.Tuple[bytes, str]:\n    body = BytesIO()\n    boundary = binascii.hexlify(os.urandom(16))\n\n    for field in iter_fields(data, files):\n        body.write(b\"--%s\\r\\n\" % boundary)\n        body.write(field.render_headers())\n        body.write(field.render_data())\n        body.write(b\"\\r\\n\")\n\n    body.write(b\"--%s--\\r\\n\" % boundary)\n\n    content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\"ascii\")\n\n    return body.getvalue(), content_type\n", "path": "httpx/multipart.py"}], "after_files": [{"content": "import binascii\nimport mimetypes\nimport os\nimport re\nimport typing\nfrom io import BytesIO\n\n_HTML5_FORM_ENCODING_REPLACEMENTS = {'\"': \"%22\", \"\\\\\": \"\\\\\\\\\"}\n_HTML5_FORM_ENCODING_REPLACEMENTS.update(\n    {chr(c): \"%{:02X}\".format(c) for c in range(0x00, 0x1F + 1) if c != 0x1B}\n)\n_HTML5_FORM_ENCODING_RE = re.compile(\n    r\"|\".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])\n)\n\n\nclass Field:\n    def render_headers(self) -> bytes:\n        raise NotImplementedError()  # pragma: nocover\n\n    def render_data(self) -> bytes:\n        raise NotImplementedError()  # pragma: nocover\n\n\nclass DataField(Field):\n    def __init__(self, name: str, value: typing.Union[str, bytes]) -> None:\n        if not isinstance(name, str):\n            raise TypeError(\"Invalid type for name. Expected str.\")\n        if not isinstance(value, (str, bytes)):\n            raise TypeError(\"Invalid type for value. Expected str or bytes.\")\n        self.name = name\n        self.value = value\n\n    def render_headers(self) -> bytes:\n        name = _format_param(\"name\", self.name)\n        return b\"\".join([b\"Content-Disposition: form-data; \", name, b\"\\r\\n\\r\\n\"])\n\n    def render_data(self) -> bytes:\n        return (\n            self.value if isinstance(self.value, bytes) else self.value.encode(\"utf-8\")\n        )\n\n\nclass FileField(Field):\n    def __init__(\n        self, name: str, value: typing.Union[typing.IO[typing.AnyStr], tuple]\n    ) -> None:\n        self.name = name\n        if not isinstance(value, tuple):\n            self.filename = os.path.basename(getattr(value, \"name\", \"upload\"))\n            self.file = value  # type: typing.Union[typing.IO[str], typing.IO[bytes]]\n            self.content_type = self.guess_content_type()\n        else:\n            self.filename = value[0]\n            self.file = value[1]\n            self.content_type = (\n                value[2] if len(value) > 2 else self.guess_content_type()\n            )\n\n    def guess_content_type(self) -> str:\n        return mimetypes.guess_type(self.filename)[0] or \"application/octet-stream\"\n\n    def render_headers(self) -> bytes:\n        name = _format_param(\"name\", self.name)\n        filename = _format_param(\"filename\", self.filename)\n        content_type = self.content_type.encode()\n        return b\"\".join(\n            [\n                b\"Content-Disposition: form-data; \",\n                name,\n                b\"; \",\n                filename,\n                b\"\\r\\nContent-Type: \",\n                content_type,\n                b\"\\r\\n\\r\\n\",\n            ]\n        )\n\n    def render_data(self) -> bytes:\n        content = self.file.read()\n        return content.encode(\"utf-8\") if isinstance(content, str) else content\n\n\ndef iter_fields(data: dict, files: dict) -> typing.Iterator[Field]:\n    for name, value in data.items():\n        if isinstance(value, (list, dict)):\n            for item in value:\n                yield DataField(name=name, value=item)\n        else:\n            yield DataField(name=name, value=value)\n\n    for name, value in files.items():\n        yield FileField(name=name, value=value)\n\n\ndef multipart_encode(data: dict, files: dict) -> typing.Tuple[bytes, str]:\n    body = BytesIO()\n    boundary = binascii.hexlify(os.urandom(16))\n\n    for field in iter_fields(data, files):\n        body.write(b\"--%s\\r\\n\" % boundary)\n        body.write(field.render_headers())\n        body.write(field.render_data())\n        body.write(b\"\\r\\n\")\n\n    body.write(b\"--%s--\\r\\n\" % boundary)\n\n    content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\"ascii\")\n\n    return body.getvalue(), content_type\n\n\ndef _format_param(name: str, value: typing.Union[str, bytes]) -> bytes:\n    if isinstance(value, bytes):\n        value = value.decode()\n        \n    def replacer(match: typing.Match[str]) -> str:\n        return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]\n\n    value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)\n    return f'{name}=\"{value}\"'.encode()\n", "path": "httpx/multipart.py"}]}