Skip to content
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
6 changes: 6 additions & 0 deletions sdk/core/azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release History

## 1.42.0 (Unreleased)

### Bugs Fixed

- Fixed `HttpRequest` encoding multipart payloads that carry form data but no file (e.g. when the file part is optional and omitted) as `application/x-www-form-urlencoded`. Such requests can now be flagged with the internal `is_multipart_payload` keyword; the form fields are then encoded as file-less multipart parts so the request is sent as `multipart/form-data`. #39163

## 1.41.0 (2026-05-07)

### Features Added
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/azure-core/azure/core/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
# regenerated.
# --------------------------------------------------------------------------

VERSION = "1.41.0"
VERSION = "1.42.0"
16 changes: 15 additions & 1 deletion sdk/core/azure-core/azure/core/rest/_rest_py3.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def __init__(
data=data,
files=files,
json=json,
is_multipart_payload=kwargs.pop("is_multipart_payload", False),
)
self.headers: MutableMapping[str, str] = case_insensitive_dict(default_headers)
self.headers.update(headers or {})
Expand All @@ -131,6 +132,7 @@ def _set_body(
data: Optional[Dict[str, Any]] = None,
files: Optional[FilesType] = None,
json: Any = None,
is_multipart_payload: bool = False,
) -> MutableMapping[str, str]:
"""Sets the body of the request, and returns the default headers.

Expand All @@ -139,6 +141,10 @@ def _set_body(
:param dict data: Form data you want in your request body.
:param dict files: Files you want to in your request body.
:param any json: A JSON serializable object.
:param bool is_multipart_payload: Whether the payload is meant to be sent as
multipart/form-data. When True, the request is treated as multipart even if
no file is present (e.g. the file part is optional and omitted), so it is not
mislabeled as ``application/x-www-form-urlencoded``.
:return: The default headers for the request
:rtype: MutableMapping[str, str]
"""
Expand All @@ -155,7 +161,15 @@ def _set_body(
if files:
default_headers, self._files = set_multipart_body(files)
if data:
default_headers, self._data = set_urlencoded_body(data, has_files=bool(files))
if is_multipart_payload and not files:
# A multipart payload whose only file part is optional (and omitted) must
# still be sent as multipart/form-data, not application/x-www-form-urlencoded.
# Transports build the multipart body from `files`, so encode the form
# fields as file-less parts (name, (None, value)) instead of url-encoding them.
default_headers, self._files = set_multipart_body(data)
self._data = None
else:
default_headers, self._data = set_urlencoded_body(data, has_files=bool(files))
return default_headers

@property
Expand Down
29 changes: 29 additions & 0 deletions sdk/core/azure-core/tests/test_rest_http_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,35 @@ def test_url_encoded_data():
} # httpx makes this just b'test=123'. set_formdata_body is still keeping it as a dict


def test_multipart_data_without_files():
# A multipart payload whose file part is optional (and omitted) still must be sent
# as multipart, not application/x-www-form-urlencoded. See issue #39163.
# The transports build a multipart body from `files`, so the form fields are encoded
# as file-less parts and `data` is cleared.
request = HttpRequest(
"POST",
"http://example.org",
data={"test": "123"},
is_multipart_payload=True,
)

assert "Content-Type" not in request.headers
assert request.files == {"test": (None, "123")}
assert request.data is None


def test_url_encoded_data_when_not_multipart():
# Without the multipart flag, data-only requests remain urlencoded (default behavior).
request = HttpRequest(
"POST",
"http://example.org",
data={"test": "123"},
is_multipart_payload=False,
)

assert request.headers["Content-Type"] == "application/x-www-form-urlencoded"


def test_json_encoded_data():
request = HttpRequest("POST", "http://example.org", json={"test": 123})

Expand Down
Loading