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..dd76a91 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -253,6 +253,12 @@ 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"")