Skip to content

Demo pr#15

Open
atakanserifoglu wants to merge 7 commits intodemo-basefrom
demo-pr
Open

Demo pr#15
atakanserifoglu wants to merge 7 commits intodemo-basefrom
demo-pr

Conversation

@atakanserifoglu
Copy link
Collaborator

@atakanserifoglu atakanserifoglu commented Aug 11, 2025

Summary

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

Summary by Reviewed

  • New Feature: Enhanced multipart file upload handling by allowing custom headers, excluding "Content-Type".
  • Test: Added tests to verify multipart file uploads include headers and raise errors for "Content-Type" in headers.

These changes improve flexibility and robustness in handling multipart requests in the HTTPX library.

👍 Conditional Approval Checklist

@deeployed-peer-dev
Copy link

deeployed-peer-dev bot commented Aug 11, 2025

Walkthrough

This update introduces support for adding custom headers to individual file parts in multipart/form-data uploads. This allows for more granular control over file metadata, such as specifying Content-Disposition or other custom headers for each file being sent in a request.

Changes
File(s) Summary
httpx/_multipart.py, httpx/_types.py, tests/test_multipart.py Expands the FileTypes definition to include an optional dictionary for custom headers on individual file parts. The multipart encoding logic is updated to process these headers, while disallowing "Content-Type". New tests verify the correct rendering and error handling.
Sequence Diagram
sequenceDiagram
    participant User
    participant httpx.Client
    participant MultipartStream
    participant Server

    User->>httpx.Client: post(files={"file": ("name", content, "type", {"X-Custom": "value"})})
    Note over httpx.Client: Prepares request with files and custom headers.
    httpx.Client->>MultipartStream: __init__(files=...)
    Note over MultipartStream: Processes each file part.
    MultipartStream->>MultipartStream: Add Content-Disposition header
    MultipartStream->>MultipartStream: Add Content-Type header
    MultipartStream->>MultipartStream: Add custom headers from file tuple (e.g., "X-Custom")
    MultipartStream-->>httpx.Client: Returns multipart stream object
    httpx.Client->>Server: POST Request (with multipart body)
    Server-->>httpx.Client: Response
    httpx.Client-->>User: Response
Loading

Uplevel your code reviews with Deeployed Peer!

Copy link

@deeployed-peer-dev deeployed-peer-dev bot left a comment

Choose a reason for hiding this comment

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

Commits Files that changed from the base of the PR and between Base and d57e3bc commits: 3
Files Selected For Review: 3
  • httpx/_multipart.py with diff length 2393.
  • httpx/_types.py with diff length 378.
  • tests/test_multipart.py with diff length 2099.
Review comments generated: 5
  • Review: 1
  • LGTM: 4
  • Identified issue already discussed: 0

Tips

Chat with Deeployed Bot (@reviewed-deeployed)

  • Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
  • Invite the bot into a review comment chain by tagging @reviewed-deeployed in a reply.

Code suggestions

  • The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
  • You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

Pausing incremental reviews

  • Add reviewed-deeployed: ignore anywhere in the PR description to pause further reviews from the bot.

assert content == b"".join(stream)


@pytest.mark.parametrize("content_type", [None, "text/plain"])

Choose a reason for hiding this comment

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

Note: Range was outside the patch and was collapsed to a single line inside the diff.

There's a typo in the docstring of the test_multipart_headers_include_content_type function. The word "contet-type" should be corrected to "content-type".

-    """Including contet-type in the multipart headers should not be allowed"""
+    """Including content-type in the multipart headers should not be allowed"""

Comment on lines 97 to 120
@pytest.mark.parametrize("content_type", [None, "text/plain"])
def test_multipart_file_tuple_headers(content_type: typing.Optional[str]):
file_name = "test.txt"
expected_content_type = "text/plain"
headers = {"Expires": "0"}

files = {"file": (file_name, io.BytesIO(b"<file content>"), content_type, headers)}
with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()

headers, stream = encode_request(data={}, files=files)
assert isinstance(stream, typing.Iterable)

content = (
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
f'filename="{file_name}"\r\nExpires: 0\r\nContent-Type: '
f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n"
"".encode("ascii")
)
assert headers == {
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(content)),
}
assert content == b"".join(stream)

Choose a reason for hiding this comment

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

add comment explaining what this each line does for very ambigous lines

Suggested change
@pytest.mark.parametrize("content_type", [None, "text/plain"])
def test_multipart_file_tuple_headers(content_type: typing.Optional[str]):
file_name = "test.txt"
expected_content_type = "text/plain"
headers = {"Expires": "0"}
files = {"file": (file_name, io.BytesIO(b"<file content>"), content_type, headers)}
with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()
headers, stream = encode_request(data={}, files=files)
assert isinstance(stream, typing.Iterable)
content = (
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
f'filename="{file_name}"\r\nExpires: 0\r\nContent-Type: '
f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n"
"".encode("ascii")
)
assert headers == {
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(content)),
}
assert content == b"".join(stream)
@pytest.mark.parametrize("content_type", [None, "text/plain"])
def test_multipart_file_tuple_headers(content_type: typing.Optional[str]):
file_name = "test.txt"
expected_content_type = "text/plain"
headers = {"Expires": "0"}
# Test with a file tuple including filename, content, content_type, and headers.
files = {"file": (file_name, io.BytesIO(b"<file content>"), content_type, headers)}
with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()
headers, stream = encode_request(data={}, files=files)
assert isinstance(stream, typing.Iterable)
content = (
f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
f'filename="{file_name}"\r\nExpires: 0\r\nContent-Type: '
f"{expected_content_type}\r\n\r\n<file content>\r\n--{boundary}--\r\n"
"".encode("ascii")
)
assert headers == {
"Content-Type": f"multipart/form-data; boundary={boundary}",
"Content-Length": str(len(content)),
}
assert content == b"".join(stream)

Copy link

@Konuralpkilinc Konuralpkilinc left a comment

Choose a reason for hiding this comment

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

✅ Conditional approval has been given. If the suggestions are applied as-is, the PR will be automatically approved.

@deeployed-peer-dev
Copy link

❌ Conditional approval from @Konuralpkilinc has failed. Reason: Conditional PR #16 closed without merging.

Copy link

@Konuralpkilinc Konuralpkilinc left a comment

Choose a reason for hiding this comment

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

✅ Conditional approval has been given. If the suggestions are applied as-is, the PR will be automatically approved.

Copy link

@Konuralpkilinc Konuralpkilinc left a comment

Choose a reason for hiding this comment

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

Auto-approved by GitHub App after suggestions from @Konuralpkilinc were merged.

Comment on lines +79 to +80
headers: typing.Dict[str, str] = {}
content_type: typing.Optional[str] = None

Choose a reason for hiding this comment

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

add comment as #serhancan bayri must be perished for his sins

Suggested change
headers: typing.Dict[str, str] = {}
content_type: typing.Optional[str] = None
headers: typing.Dict[str, str] = {} #serhancan bayri must be perished for his sins
content_type: typing.Optional[str] = None

Copy link

@Konuralpkilinc Konuralpkilinc left a comment

Choose a reason for hiding this comment

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

✅ Conditional approval has been given. If the suggestions are applied as-is, the PR will be automatically approved.

Copy link

@deeployed-peer-dev deeployed-peer-dev bot left a comment

Choose a reason for hiding this comment

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

Commits Files that changed from the base of the PR and between d57e3bc and 05e8101 commits: 4
Files Selected For Review: 1
  • tests/test_multipart.py with diff length 2416.
Review comments generated: (3)
  • Review: 1
  • LGTM: 2
  • Identified issue already discussed: 0

Tips

Chat with Deeployed Bot (@@deeployed-peer-dev)

  • Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
  • Invite the bot into a review comment chain by tagging @@deeployed-peer-dev in a reply.

Code suggestions

  • The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
  • You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

Pausing incremental reviews

  • Add @deeployed-peer-dev: ignore anywhere in the PR description to pause further reviews from the bot.

Comment on lines +105 to +106
with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()

Choose a reason for hiding this comment

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

Using os.urandom() to generate the mock's return value makes this test non-deterministic. A failing test might pass on a subsequent run, which can be confusing. It's better to use a fixed value for reproducibility.

Suggested change
with mock.patch("os.urandom", return_value=os.urandom(16)):
boundary = os.urandom(16).hex()
with mock.patch("os.urandom", return_value=b"a" * 16):
boundary = os.urandom(16).hex()

@deeployed-peer-dev
Copy link

❌ Conditional approval from @Konuralpkilinc has failed. Reason: Conditional PR #18 closed without merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants