diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index e45f63bbe..94b6d4c8d 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -279,14 +279,22 @@ class Executor: headers[authorization.config.header] = authorization.config.api_key or "" # Handle Content-Type for multipart/form-data requests - # Fix for issue #22880: Missing boundary when using multipart/form-data + # Fix for issue #23829: Missing boundary when using multipart/form-data body = self.node_data.body if body and body.type == "form-data": - # For multipart/form-data with files, let httpx handle the boundary automatically - # by not setting Content-Type header when files are present - if not self.files or all(f[0] == "__multipart_placeholder__" for f in self.files): - # Only set Content-Type when there are no actual files - # This ensures httpx generates the correct boundary + # For multipart/form-data with files (including placeholder files), + # remove any manually set Content-Type header to let httpx handle + # For multipart/form-data, if any files are present (including placeholder files), + # we must remove any manually set Content-Type header. This is because httpx needs to + # automatically set the Content-Type and boundary for multipart encoding whenever files + # are included, even if they are placeholders, to avoid boundary issues and ensure correct + # file upload behaviour. Manually setting Content-Type can cause httpx to fail to set the + # boundary, resulting in invalid requests. + if self.files: + # Remove Content-Type if it was manually set to avoid boundary issues + headers = {k: v for k, v in headers.items() if k.lower() != "content-type"} + else: + # No files at all, set Content-Type manually if "content-type" not in (k.lower() for k in headers): headers["Content-Type"] = "multipart/form-data" elif body and body.type in BODY_TYPE_TO_CONTENT_TYPE: diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py index 3101f7dd3..8b5a82fcb 100644 --- a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py @@ -243,8 +243,6 @@ def test_executor_with_form_data(): # Check the executor's data assert executor.method == "post" assert executor.url == "https://api.example.com/upload" - assert "Content-Type" in executor.headers - assert "multipart/form-data" in executor.headers["Content-Type"] assert executor.params is None assert executor.json is None # '__multipart_placeholder__' is expected when no file inputs exist, @@ -252,6 +250,11 @@ def test_executor_with_form_data(): assert executor.files == [("__multipart_placeholder__", ("", b"", "application/octet-stream"))] assert executor.content is None + # After fix for #23829: When placeholder files exist, Content-Type is removed + # to let httpx handle Content-Type and boundary automatically + headers = executor._assembling_headers() + assert "Content-Type" not in headers or "multipart/form-data" not in headers.get("Content-Type", "") + # Check that the form data is correctly loaded in executor.data assert isinstance(executor.data, dict) assert "text_field" in executor.data