Prechádzať zdrojové kódy

fix: change http node params from dict to list tuple (#11665)

非法操作 4 mesiacov pred
rodič
commit
9c7a1bc067

+ 41 - 34
api/core/workflow/nodes/http_request/executor.py

@@ -37,7 +37,7 @@ BODY_TYPE_TO_CONTENT_TYPE = {
 class Executor:
     method: Literal["get", "head", "post", "put", "delete", "patch"]
     url: str
-    params: Mapping[str, str] | None
+    params: list[tuple[str, str]] | None
     content: str | bytes | None
     data: Mapping[str, Any] | None
     files: Mapping[str, tuple[str | None, bytes, str]] | None
@@ -67,7 +67,7 @@ class Executor:
         self.method = node_data.method
         self.auth = node_data.authorization
         self.timeout = timeout
-        self.params = {}
+        self.params = []
         self.headers = {}
         self.content = None
         self.files = None
@@ -89,14 +89,48 @@ class Executor:
         self.url = self.variable_pool.convert_template(self.node_data.url).text
 
     def _init_params(self):
-        params = _plain_text_to_dict(self.node_data.params)
-        for key in params:
-            params[key] = self.variable_pool.convert_template(params[key]).text
-        self.params = params
+        """
+        Almost same as _init_headers(), difference:
+        1. response a list tuple to support same key, like 'aa=1&aa=2'
+        2. param value may have '\n', we need to splitlines then extract the variable value.
+        """
+        result = []
+        for line in self.node_data.params.splitlines():
+            if not (line := line.strip()):
+                continue
+
+            key, *value = line.split(":", 1)
+            if not (key := key.strip()):
+                continue
+
+            value = value[0].strip() if value else ""
+            result.append(
+                (self.variable_pool.convert_template(key).text, self.variable_pool.convert_template(value).text)
+            )
+
+        self.params = result
 
     def _init_headers(self):
+        """
+        Convert the header string of frontend to a dictionary.
+
+        Each line in the header string represents a key-value pair.
+        Keys and values are separated by ':'.
+        Empty values are allowed.
+
+        Examples:
+            'aa:bb\n cc:dd'  -> {'aa': 'bb', 'cc': 'dd'}
+            'aa:\n cc:dd\n'  -> {'aa': '', 'cc': 'dd'}
+            'aa\n cc : dd'   -> {'aa': '', 'cc': 'dd'}
+
+        """
         headers = self.variable_pool.convert_template(self.node_data.headers).text
-        self.headers = _plain_text_to_dict(headers)
+        self.headers = {
+            key.strip(): (value[0].strip() if value else "")
+            for line in headers.splitlines()
+            if line.strip()
+            for key, *value in [line.split(":", 1)]
+        }
 
     def _init_body(self):
         body = self.node_data.body
@@ -288,33 +322,6 @@ class Executor:
         return raw
 
 
-def _plain_text_to_dict(text: str, /) -> dict[str, str]:
-    """
-    Convert a string of key-value pairs to a dictionary.
-
-    Each line in the input string represents a key-value pair.
-    Keys and values are separated by ':'.
-    Empty values are allowed.
-
-    Examples:
-        'aa:bb\n cc:dd'  -> {'aa': 'bb', 'cc': 'dd'}
-        'aa:\n cc:dd\n'  -> {'aa': '', 'cc': 'dd'}
-        'aa\n cc : dd'   -> {'aa': '', 'cc': 'dd'}
-
-    Args:
-        convert_text (str): The input string to convert.
-
-    Returns:
-        dict[str, str]: A dictionary of key-value pairs.
-    """
-    return {
-        key.strip(): (value[0].strip() if value else "")
-        for line in text.splitlines()
-        if line.strip()
-        for key, *value in [line.split(":", 1)]
-    }
-
-
 def _generate_random_string(n: int) -> str:
     """
     Generate a random string of lowercase ASCII letters.

+ 74 - 5
api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py

@@ -48,7 +48,7 @@ def test_executor_with_json_body_and_number_variable():
     assert executor.method == "post"
     assert executor.url == "https://api.example.com/data"
     assert executor.headers == {"Content-Type": "application/json"}
-    assert executor.params == {}
+    assert executor.params == []
     assert executor.json == {"number": 42}
     assert executor.data is None
     assert executor.files is None
@@ -101,7 +101,7 @@ def test_executor_with_json_body_and_object_variable():
     assert executor.method == "post"
     assert executor.url == "https://api.example.com/data"
     assert executor.headers == {"Content-Type": "application/json"}
-    assert executor.params == {}
+    assert executor.params == []
     assert executor.json == {"name": "John Doe", "age": 30, "email": "john@example.com"}
     assert executor.data is None
     assert executor.files is None
@@ -156,7 +156,7 @@ def test_executor_with_json_body_and_nested_object_variable():
     assert executor.method == "post"
     assert executor.url == "https://api.example.com/data"
     assert executor.headers == {"Content-Type": "application/json"}
-    assert executor.params == {}
+    assert executor.params == []
     assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "john@example.com"}}
     assert executor.data is None
     assert executor.files is None
@@ -195,7 +195,7 @@ def test_extract_selectors_from_template_with_newline():
         variable_pool=variable_pool,
     )
 
-    assert executor.params == {"test": "line1\nline2"}
+    assert executor.params == [("test", "line1\nline2")]
 
 
 def test_executor_with_form_data():
@@ -244,7 +244,7 @@ def test_executor_with_form_data():
     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 == {}
+    assert executor.params == []
     assert executor.json is None
     assert executor.files is None
     assert executor.content is None
@@ -265,3 +265,72 @@ def test_executor_with_form_data():
     assert "Hello, World!" in raw_request
     assert "number_field" in raw_request
     assert "42" in raw_request
+
+
+def test_init_headers():
+    def create_executor(headers: str) -> Executor:
+        node_data = HttpRequestNodeData(
+            title="test",
+            method="get",
+            url="http://example.com",
+            headers=headers,
+            params="",
+            authorization=HttpRequestNodeAuthorization(type="no-auth"),
+        )
+        timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30)
+        return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool())
+
+    executor = create_executor("aa\n cc:")
+    executor._init_headers()
+    assert executor.headers == {"aa": "", "cc": ""}
+
+    executor = create_executor("aa:bb\n cc:dd")
+    executor._init_headers()
+    assert executor.headers == {"aa": "bb", "cc": "dd"}
+
+    executor = create_executor("aa:bb\n cc:dd\n")
+    executor._init_headers()
+    assert executor.headers == {"aa": "bb", "cc": "dd"}
+
+    executor = create_executor("aa:bb\n\n cc : dd\n\n")
+    executor._init_headers()
+    assert executor.headers == {"aa": "bb", "cc": "dd"}
+
+
+def test_init_params():
+    def create_executor(params: str) -> Executor:
+        node_data = HttpRequestNodeData(
+            title="test",
+            method="get",
+            url="http://example.com",
+            headers="",
+            params=params,
+            authorization=HttpRequestNodeAuthorization(type="no-auth"),
+        )
+        timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30)
+        return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool())
+
+    # Test basic key-value pairs
+    executor = create_executor("key1:value1\nkey2:value2")
+    executor._init_params()
+    assert executor.params == [("key1", "value1"), ("key2", "value2")]
+
+    # Test empty values
+    executor = create_executor("key1:\nkey2:")
+    executor._init_params()
+    assert executor.params == [("key1", ""), ("key2", "")]
+
+    # Test duplicate keys (which is allowed for params)
+    executor = create_executor("key1:value1\nkey1:value2")
+    executor._init_params()
+    assert executor.params == [("key1", "value1"), ("key1", "value2")]
+
+    # Test whitespace handling
+    executor = create_executor(" key1 : value1 \n key2 : value2 ")
+    executor._init_params()
+    assert executor.params == [("key1", "value1"), ("key2", "value2")]
+
+    # Test empty lines and extra whitespace
+    executor = create_executor("key1:value1\n\nkey2:value2\n\n")
+    executor._init_params()
+    assert executor.params == [("key1", "value1"), ("key2", "value2")]

+ 0 - 8
api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py

@@ -14,18 +14,10 @@ from core.workflow.nodes.http_request import (
     HttpRequestNodeBody,
     HttpRequestNodeData,
 )
-from core.workflow.nodes.http_request.executor import _plain_text_to_dict
 from models.enums import UserFrom
 from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
 
 
-def test_plain_text_to_dict():
-    assert _plain_text_to_dict("aa\n cc:") == {"aa": "", "cc": ""}
-    assert _plain_text_to_dict("aa:bb\n cc:dd") == {"aa": "bb", "cc": "dd"}
-    assert _plain_text_to_dict("aa:bb\n cc:dd\n") == {"aa": "bb", "cc": "dd"}
-    assert _plain_text_to_dict("aa:bb\n\n cc : dd\n\n") == {"aa": "bb", "cc": "dd"}
-
-
 def test_http_request_node_binary_file(monkeypatch):
     data = HttpRequestNodeData(
         title="test",