Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat headers case insenitively, internally #190

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 12 additions & 9 deletions python_multipart/multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .exceptions import FileError, FormParserError, MultipartParseError, QuerystringParseError

if TYPE_CHECKING: # pragma: no cover
from typing import Any, Callable, Literal, Protocol, TypedDict
from typing import Any, Callable, Literal, Mapping, Protocol, TypedDict

from typing_extensions import TypeAlias

Expand Down Expand Up @@ -1617,7 +1617,8 @@ def _on_end() -> None:

header_name: list[bytes] = []
header_value: list[bytes] = []
headers: dict[bytes, bytes] = {}
# Header keys are always inserted in Title-Case
headers: dict[str, bytes] = {}

f_multi: FileProtocol | FieldProtocol | None = None
writer = None
Expand Down Expand Up @@ -1652,7 +1653,9 @@ def on_header_value(data: bytes, start: int, end: int) -> None:
header_value.append(data[start:end])

def on_header_end() -> None:
headers[b"".join(header_name)] = b"".join(header_value)
# Convert header name to title case.
header_name_tc = b"".join(header_name).decode().title()
headers[header_name_tc] = b"".join(header_value)
Comment on lines +1656 to +1658
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we actually lower case instead of title?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either would work, there's a trade off.

Lower case will require more of the code and tests to change as they currently expect title case. It also impacts the outside interface, where we say "The only required header is Content-Type." and don't say anything about the case-sensitivity of the input dict. I don't want to have to re-ingest the input header dict to ensure it's case insensitive. Switching to using only the content-type header value here would help, but entirely breaks the external API.

del header_name[:]
del header_value[:]

Expand All @@ -1662,8 +1665,7 @@ def on_headers_finished() -> None:
is_file = False

# Parse the content-disposition header.
# TODO: handle mixed case
content_disp = headers.get(b"Content-Disposition")
content_disp = headers.get("Content-Disposition")
disp, options = parse_options_header(content_disp)

# Get the field and filename.
Expand All @@ -1681,7 +1683,7 @@ def on_headers_finished() -> None:
# Parse the given Content-Transfer-Encoding to determine what
# we need to do with the incoming data.
# TODO: check that we properly handle 8bit / 7bit encoding.
transfer_encoding = headers.get(b"Content-Transfer-Encoding", b"7bit")
transfer_encoding = headers.get("Content-Transfer-Encoding", b"7bit")

if transfer_encoding in (b"binary", b"8bit", b"7bit"):
writer = f_multi
Expand Down Expand Up @@ -1760,7 +1762,7 @@ def __repr__(self) -> str:


def create_form_parser(
headers: dict[str, bytes],
headers: Mapping[str, bytes],
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
trust_x_headers: bool = False,
Expand Down Expand Up @@ -1804,7 +1806,7 @@ def create_form_parser(


def parse_form(
headers: dict[str, bytes],
headers: Mapping[str, bytes],
input_stream: SupportsRead,
on_field: OnFieldCallback | None,
on_file: OnFileCallback | None,
Expand All @@ -1816,7 +1818,8 @@ def parse_form(
callbacks that will get called whenever a field or file is parsed.

Args:
headers: A dictionary-like object of HTTP headers. The only required header is Content-Type.
headers: A dictionary-like object of HTTP headers. The only required header is Content-Type,
in exactly this form if the input dict is case sensitive.
input_stream: A file-like object that represents the request body. The read() method must return bytestrings.
on_field: Callback to call with each parsed field.
on_file: Callback to call with each parsed file.
Expand Down
19 changes: 19 additions & 0 deletions tests/test_data/http/mixed_case_headers.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
----boundary
ConTenT-TypE: text/plain; charset="UTF-8"
ConTenT-DisPoSitioN: form-data; name=field1
ConTenT-TransfeR-EncoDinG: base64

VGVzdCAxMjM=
----boundary
content-type: text/plain; charset="UTF-8"
content-disposition: form-data; name=field2
content-transfer-encoding: base64

VGVzdCAxMjM=
----boundary
CONTENT-TYPE: text/plain; charset="UTF-8"
CONTENT-DISPOSITION: form-data; name=Field3
CONTENT-TRANSFER-ENCODING: base64

VGVzdCAxMjM=
----boundary--
14 changes: 14 additions & 0 deletions tests/test_data/http/mixed_case_headers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
boundary: --boundary
expected:
- name: field1
type: field
data: !!binary |
VGVzdCAxMjM=
- name: field2
type: field
data: !!binary |
VGVzdCAxMjM=
- name: Field3
type: field
data: !!binary |
VGVzdCAxMjM=