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
8 changes: 8 additions & 0 deletions src/cloudflare/resources/kv/namespaces/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ def update(
raise ValueError(f"Expected a non-empty value for `namespace_id` but received {namespace_id!r}")
if not key_name:
raise ValueError(f"Expected a non-empty value for `key_name` but received {key_name!r}")
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
# multipart/form-data; boundary=---abc--
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return self._put(
f"/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}",
body=maybe_transform(
Expand Down Expand Up @@ -324,6 +328,10 @@ async def update(
raise ValueError(f"Expected a non-empty value for `namespace_id` but received {namespace_id!r}")
if not key_name:
raise ValueError(f"Expected a non-empty value for `key_name` but received {key_name!r}")
# It should be noted that the actual Content-Type header that will be
# sent to the server will contain a `boundary` parameter, e.g.
# multipart/form-data; boundary=---abc--
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
return await self._put(
f"/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}",
body=await async_maybe_transform(
Expand Down
68 changes: 68 additions & 0 deletions tests/api_resources/kv/namespaces/test_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,40 @@ def test_path_params_update(self, client: Cloudflare) -> None:
value="Some Value",
)

@parametrize
@pytest.mark.respx(base_url=base_url)
def test_update_uses_multipart_form_data(self, client: Cloudflare, respx_mock: MockRouter) -> None:
"""Test that values.update sends data as multipart/form-data, not JSON.

This is a regression test for https://github.com/cloudflare/cloudflare-python/issues/2519
"""
respx_mock.put(
"/accounts/023e105f4ecef8ad9ca31a8372d0c353/storage/kv/namespaces/0f2ac74b498b48028cb68387c421e279/values/My-Key"
).mock(return_value=httpx.Response(200, json={"success": True, "result": {}}))

client.kv.namespaces.values.update(
key_name="My-Key",
account_id="023e105f4ecef8ad9ca31a8372d0c353",
namespace_id="0f2ac74b498b48028cb68387c421e279",
metadata='{"someMetadataKey": "someMetadataValue"}',
value="Some Value",
)

# Verify the request was made
assert respx_mock.calls.call_count == 1
request = respx_mock.calls[0].request

# Verify Content-Type is multipart/form-data (with boundary)
content_type = request.headers.get("content-type", "")
assert content_type.startswith("multipart/form-data"), (
f"Expected Content-Type to start with 'multipart/form-data', got '{content_type}'"
)

# Verify the body contains separate form fields for value and metadata
body = request.content.decode("utf-8")
assert "Some Value" in body, "Value should be in the multipart body"
assert "someMetadataKey" in body, "Metadata should be in the multipart body"

@parametrize
def test_method_delete(self, client: Cloudflare) -> None:
value = client.kv.namespaces.values.delete(
Expand Down Expand Up @@ -351,6 +385,40 @@ async def test_path_params_update(self, async_client: AsyncCloudflare) -> None:
value="Some Value",
)

@parametrize
@pytest.mark.respx(base_url=base_url)
async def test_update_uses_multipart_form_data(self, async_client: AsyncCloudflare, respx_mock: MockRouter) -> None:
"""Test that values.update sends data as multipart/form-data, not JSON.

This is a regression test for https://github.com/cloudflare/cloudflare-python/issues/2519
"""
respx_mock.put(
"/accounts/023e105f4ecef8ad9ca31a8372d0c353/storage/kv/namespaces/0f2ac74b498b48028cb68387c421e279/values/My-Key"
).mock(return_value=httpx.Response(200, json={"success": True, "result": {}}))

await async_client.kv.namespaces.values.update(
key_name="My-Key",
account_id="023e105f4ecef8ad9ca31a8372d0c353",
namespace_id="0f2ac74b498b48028cb68387c421e279",
metadata='{"someMetadataKey": "someMetadataValue"}',
value="Some Value",
)

# Verify the request was made
assert respx_mock.calls.call_count == 1
request = respx_mock.calls[0].request

# Verify Content-Type is multipart/form-data (with boundary)
content_type = request.headers.get("content-type", "")
assert content_type.startswith("multipart/form-data"), (
f"Expected Content-Type to start with 'multipart/form-data', got '{content_type}'"
)

# Verify the body contains separate form fields for value and metadata
body = request.content.decode("utf-8")
assert "Some Value" in body, "Value should be in the multipart body"
assert "someMetadataKey" in body, "Metadata should be in the multipart body"

@parametrize
async def test_method_delete(self, async_client: AsyncCloudflare) -> None:
value = await async_client.kv.namespaces.values.delete(
Expand Down