|
14 | 14 | from .force_multipart import FORCE_MULTIPART |
15 | 15 | from .jsonable_encoder import jsonable_encoder |
16 | 16 | from .query_encoder import encode_query |
17 | | -from .remove_none_from_dict import remove_none_from_dict |
| 17 | +from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict |
18 | 18 | from .request_options import RequestOptions |
19 | 19 | from httpx._types import RequestFiles |
20 | 20 |
|
@@ -123,6 +123,21 @@ def _should_retry(response: httpx.Response) -> bool: |
123 | 123 | return response.status_code >= 500 or response.status_code in retryable_400s |
124 | 124 |
|
125 | 125 |
|
| 126 | +def _maybe_filter_none_from_multipart_data( |
| 127 | + data: typing.Optional[typing.Any], |
| 128 | + request_files: typing.Optional[RequestFiles], |
| 129 | + force_multipart: typing.Optional[bool], |
| 130 | +) -> typing.Optional[typing.Any]: |
| 131 | + """ |
| 132 | + Filter None values from data body for multipart/form requests. |
| 133 | + This prevents httpx from converting None to empty strings in multipart encoding. |
| 134 | + Only applies when files are present or force_multipart is True. |
| 135 | + """ |
| 136 | + if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): |
| 137 | + return remove_none_from_dict(data) |
| 138 | + return data |
| 139 | + |
| 140 | + |
126 | 141 | def remove_omit_from_dict( |
127 | 142 | original: typing.Dict[str, typing.Optional[typing.Any]], |
128 | 143 | omit: typing.Optional[typing.Any], |
@@ -244,6 +259,8 @@ def request( |
244 | 259 | if (request_files is None or len(request_files) == 0) and force_multipart: |
245 | 260 | request_files = FORCE_MULTIPART |
246 | 261 |
|
| 262 | + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) |
| 263 | + |
247 | 264 | response = self.httpx_client.request( |
248 | 265 | method=method, |
249 | 266 | url=urllib.parse.urljoin(f"{base_url}/", path), |
@@ -341,6 +358,8 @@ def stream( |
341 | 358 |
|
342 | 359 | json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) |
343 | 360 |
|
| 361 | + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) |
| 362 | + |
344 | 363 | with self.httpx_client.stream( |
345 | 364 | method=method, |
346 | 365 | url=urllib.parse.urljoin(f"{base_url}/", path), |
@@ -442,6 +461,8 @@ async def request( |
442 | 461 |
|
443 | 462 | json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) |
444 | 463 |
|
| 464 | + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) |
| 465 | + |
445 | 466 | # Add the input to each of these and do None-safety checks |
446 | 467 | response = await self.httpx_client.request( |
447 | 468 | method=method, |
@@ -539,6 +560,8 @@ async def stream( |
539 | 560 |
|
540 | 561 | json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) |
541 | 562 |
|
| 563 | + data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) |
| 564 | + |
542 | 565 | async with self.httpx_client.stream( |
543 | 566 | method=method, |
544 | 567 | url=urllib.parse.urljoin(f"{base_url}/", path), |
|
0 commit comments