Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 16 additions & 3 deletions python_multipart/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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] = {}
Expand All @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions tests/test_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"")
Expand Down
Loading