Skip to content

Commit 2b0ed9e

Browse files
author
Chris Thorwarth
committed
fix(kv): use multipart/form-data for values.update() requests
1 parent ac1ae1c commit 2b0ed9e

2 files changed

Lines changed: 76 additions & 0 deletions

File tree

src/cloudflare/resources/kv/namespaces/values.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ def update(
113113
raise ValueError(f"Expected a non-empty value for `namespace_id` but received {namespace_id!r}")
114114
if not key_name:
115115
raise ValueError(f"Expected a non-empty value for `key_name` but received {key_name!r}")
116+
# It should be noted that the actual Content-Type header that will be
117+
# sent to the server will contain a `boundary` parameter, e.g.
118+
# multipart/form-data; boundary=---abc--
119+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
116120
return self._put(
117121
f"/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}",
118122
body=maybe_transform(
@@ -324,6 +328,10 @@ async def update(
324328
raise ValueError(f"Expected a non-empty value for `namespace_id` but received {namespace_id!r}")
325329
if not key_name:
326330
raise ValueError(f"Expected a non-empty value for `key_name` but received {key_name!r}")
331+
# It should be noted that the actual Content-Type header that will be
332+
# sent to the server will contain a `boundary` parameter, e.g.
333+
# multipart/form-data; boundary=---abc--
334+
extra_headers = {"Content-Type": "multipart/form-data", **(extra_headers or {})}
327335
return await self._put(
328336
f"/accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name}",
329337
body=await async_maybe_transform(

tests/api_resources/kv/namespaces/test_values.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,40 @@ def test_path_params_update(self, client: Cloudflare) -> None:
115115
value="Some Value",
116116
)
117117

118+
@parametrize
119+
@pytest.mark.respx(base_url=base_url)
120+
def test_update_uses_multipart_form_data(self, client: Cloudflare, respx_mock: MockRouter) -> None:
121+
"""Test that values.update sends data as multipart/form-data, not JSON.
122+
123+
This is a regression test for https://github.com/cloudflare/cloudflare-python/issues/2519
124+
"""
125+
respx_mock.put(
126+
"/accounts/023e105f4ecef8ad9ca31a8372d0c353/storage/kv/namespaces/0f2ac74b498b48028cb68387c421e279/values/My-Key"
127+
).mock(return_value=httpx.Response(200, json={"success": True, "result": {}}))
128+
129+
client.kv.namespaces.values.update(
130+
key_name="My-Key",
131+
account_id="023e105f4ecef8ad9ca31a8372d0c353",
132+
namespace_id="0f2ac74b498b48028cb68387c421e279",
133+
metadata='{"someMetadataKey": "someMetadataValue"}',
134+
value="Some Value",
135+
)
136+
137+
# Verify the request was made
138+
assert respx_mock.calls.call_count == 1
139+
request = respx_mock.calls[0].request
140+
141+
# Verify Content-Type is multipart/form-data (with boundary)
142+
content_type = request.headers.get("content-type", "")
143+
assert content_type.startswith("multipart/form-data"), (
144+
f"Expected Content-Type to start with 'multipart/form-data', got '{content_type}'"
145+
)
146+
147+
# Verify the body contains separate form fields for value and metadata
148+
body = request.content.decode("utf-8")
149+
assert "Some Value" in body, "Value should be in the multipart body"
150+
assert "someMetadataKey" in body, "Metadata should be in the multipart body"
151+
118152
@parametrize
119153
def test_method_delete(self, client: Cloudflare) -> None:
120154
value = client.kv.namespaces.values.delete(
@@ -351,6 +385,40 @@ async def test_path_params_update(self, async_client: AsyncCloudflare) -> None:
351385
value="Some Value",
352386
)
353387

388+
@parametrize
389+
@pytest.mark.respx(base_url=base_url)
390+
async def test_update_uses_multipart_form_data(self, async_client: AsyncCloudflare, respx_mock: MockRouter) -> None:
391+
"""Test that values.update sends data as multipart/form-data, not JSON.
392+
393+
This is a regression test for https://github.com/cloudflare/cloudflare-python/issues/2519
394+
"""
395+
respx_mock.put(
396+
"/accounts/023e105f4ecef8ad9ca31a8372d0c353/storage/kv/namespaces/0f2ac74b498b48028cb68387c421e279/values/My-Key"
397+
).mock(return_value=httpx.Response(200, json={"success": True, "result": {}}))
398+
399+
await async_client.kv.namespaces.values.update(
400+
key_name="My-Key",
401+
account_id="023e105f4ecef8ad9ca31a8372d0c353",
402+
namespace_id="0f2ac74b498b48028cb68387c421e279",
403+
metadata='{"someMetadataKey": "someMetadataValue"}',
404+
value="Some Value",
405+
)
406+
407+
# Verify the request was made
408+
assert respx_mock.calls.call_count == 1
409+
request = respx_mock.calls[0].request
410+
411+
# Verify Content-Type is multipart/form-data (with boundary)
412+
content_type = request.headers.get("content-type", "")
413+
assert content_type.startswith("multipart/form-data"), (
414+
f"Expected Content-Type to start with 'multipart/form-data', got '{content_type}'"
415+
)
416+
417+
# Verify the body contains separate form fields for value and metadata
418+
body = request.content.decode("utf-8")
419+
assert "Some Value" in body, "Value should be in the multipart body"
420+
assert "someMetadataKey" in body, "Metadata should be in the multipart body"
421+
354422
@parametrize
355423
async def test_method_delete(self, async_client: AsyncCloudflare) -> None:
356424
value = await async_client.kv.namespaces.values.delete(

0 commit comments

Comments
 (0)