From 420c236ae4ef35c0a61d8fadb69e1ca0a980511f Mon Sep 17 00:00:00 2001 From: Gyanu Date: Sat, 22 Aug 2026 23:10:42 +0530 Subject: [PATCH 1/2] Accept Unicode str values in parse_options_header. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function already takes str, but encoding as latin-1 raised UnicodeEncodeError for filenames like 中文.doc. Fall back to UTF-8 when latin-1 cannot represent the value. --- CHANGELOG.md | 1 + python_multipart/multipart.py | 19 ++++++++++++++++--- tests/test_multipart.py | 8 ++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb17fe..01eb624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* Accept Unicode ``str`` values in ``parse_options_header`` instead of raising ``UnicodeEncodeError`` [#319](https://github.com/Kludex/python-multipart/issues/319). * Speed up querystring callback dispatch [#316](https://github.com/Kludex/python-multipart/pull/316). ## 0.0.32 (2026-06-04) diff --git a/python_multipart/multipart.py b/python_multipart/multipart.py index 49cdd8e..d31c299 100644 --- a/python_multipart/multipart.py +++ b/python_multipart/multipart.py @@ -193,6 +193,19 @@ def _parseparam(s: str) -> list[str]: return plist +def _encode_header_token(value: str) -> bytes: + """Encode a header token from a Python ``str``. + + HTTP header fields are ISO-8859-1. Values that cannot be encoded that way + (for example a Unicode filename passed as ``str``) are encoded as UTF-8 so + the public ``str`` API does not raise ``UnicodeEncodeError``. + """ + try: + return value.encode("latin-1") + except UnicodeEncodeError: + return value.encode("utf-8") + + def parse_options_header(value: str | bytes | None) -> tuple[bytes, dict[bytes, bytes]]: """Parses a Content-Type header into a value in the following format: (content_type, {parameters}).""" if not value: @@ -207,7 +220,7 @@ def parse_options_header(value: str | bytes | None) -> tuple[bytes, dict[bytes, # If we have no options, return the string as-is. if ";" not in value: - return (value.lower().strip().encode("latin-1"), {}) + return (_encode_header_token(value.lower().strip()), {}) ctype, *segments = _parseparam(value) options: dict[bytes, bytes] = {} @@ -225,8 +238,8 @@ def parse_options_header(value: str | bytes | None) -> tuple[bytes, dict[bytes, # just the filename. if key == "filename" and (val[1:3] == ":\\" or val[:2] == "\\\\"): val = val.split("\\")[-1] - options[key.encode("latin-1")] = val.encode("latin-1") - return ctype.encode("latin-1"), options + options[_encode_header_token(key)] = _encode_header_token(val) + return _encode_header_token(ctype), options class Field: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 949d70c..0444e14 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -253,6 +253,14 @@ def test_simple(self) -> None: self.assertEqual(t, b"application/json") self.assertEqual(p, {}) + def test_unicode_filename_str(self) -> None: + t, p = parse_options_header( + 'form-data; name="upload"; filename="中文.doc"' + ) + self.assertEqual(t, b"form-data") + self.assertEqual(p[b"name"], b"upload") + self.assertEqual(p[b"filename"], "中文.doc".encode("utf-8")) + def test_blank(self) -> None: t, p = parse_options_header("") self.assertEqual(t, b"") From 4b35e58f3b68f56773c8850c2e6f0fc39ceac243 Mon Sep 17 00:00:00 2001 From: Gyanu Date: Sat, 22 Aug 2026 23:52:34 +0530 Subject: [PATCH 2/2] Format the Unicode filename test for ruff. --- tests/test_multipart.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 0444e14..dd76a91 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -254,9 +254,7 @@ def test_simple(self) -> None: self.assertEqual(p, {}) def test_unicode_filename_str(self) -> None: - t, p = parse_options_header( - 'form-data; name="upload"; filename="中文.doc"' - ) + t, p = parse_options_header('form-data; name="upload"; filename="中文.doc"') self.assertEqual(t, b"form-data") self.assertEqual(p[b"name"], b"upload") self.assertEqual(p[b"filename"], "中文.doc".encode("utf-8"))