Skip to content

Commit 66d7d7a

Browse files
hjlarryiamjoel
authored andcommitted
fix: change http node params from dict to list tuple (#11665)
1 parent ae45575 commit 66d7d7a

File tree

3 files changed

+115
-47
lines changed

3 files changed

+115
-47
lines changed

api/core/workflow/nodes/http_request/executor.py

+41-34
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
class Executor:
3838
method: Literal["get", "head", "post", "put", "delete", "patch"]
3939
url: str
40-
params: Mapping[str, str] | None
40+
params: list[tuple[str, str]] | None
4141
content: str | bytes | None
4242
data: Mapping[str, Any] | None
4343
files: Mapping[str, tuple[str | None, bytes, str]] | None
@@ -67,7 +67,7 @@ def __init__(
6767
self.method = node_data.method
6868
self.auth = node_data.authorization
6969
self.timeout = timeout
70-
self.params = {}
70+
self.params = []
7171
self.headers = {}
7272
self.content = None
7373
self.files = None
@@ -89,14 +89,48 @@ def _init_url(self):
8989
self.url = self.variable_pool.convert_template(self.node_data.url).text
9090

9191
def _init_params(self):
92-
params = _plain_text_to_dict(self.node_data.params)
93-
for key in params:
94-
params[key] = self.variable_pool.convert_template(params[key]).text
95-
self.params = params
92+
"""
93+
Almost same as _init_headers(), difference:
94+
1. response a list tuple to support same key, like 'aa=1&aa=2'
95+
2. param value may have '\n', we need to splitlines then extract the variable value.
96+
"""
97+
result = []
98+
for line in self.node_data.params.splitlines():
99+
if not (line := line.strip()):
100+
continue
101+
102+
key, *value = line.split(":", 1)
103+
if not (key := key.strip()):
104+
continue
105+
106+
value = value[0].strip() if value else ""
107+
result.append(
108+
(self.variable_pool.convert_template(key).text, self.variable_pool.convert_template(value).text)
109+
)
110+
111+
self.params = result
96112

97113
def _init_headers(self):
114+
"""
115+
Convert the header string of frontend to a dictionary.
116+
117+
Each line in the header string represents a key-value pair.
118+
Keys and values are separated by ':'.
119+
Empty values are allowed.
120+
121+
Examples:
122+
'aa:bb\n cc:dd' -> {'aa': 'bb', 'cc': 'dd'}
123+
'aa:\n cc:dd\n' -> {'aa': '', 'cc': 'dd'}
124+
'aa\n cc : dd' -> {'aa': '', 'cc': 'dd'}
125+
126+
"""
98127
headers = self.variable_pool.convert_template(self.node_data.headers).text
99-
self.headers = _plain_text_to_dict(headers)
128+
self.headers = {
129+
key.strip(): (value[0].strip() if value else "")
130+
for line in headers.splitlines()
131+
if line.strip()
132+
for key, *value in [line.split(":", 1)]
133+
}
100134

101135
def _init_body(self):
102136
body = self.node_data.body
@@ -288,33 +322,6 @@ def to_log(self):
288322
return raw
289323

290324

291-
def _plain_text_to_dict(text: str, /) -> dict[str, str]:
292-
"""
293-
Convert a string of key-value pairs to a dictionary.
294-
295-
Each line in the input string represents a key-value pair.
296-
Keys and values are separated by ':'.
297-
Empty values are allowed.
298-
299-
Examples:
300-
'aa:bb\n cc:dd' -> {'aa': 'bb', 'cc': 'dd'}
301-
'aa:\n cc:dd\n' -> {'aa': '', 'cc': 'dd'}
302-
'aa\n cc : dd' -> {'aa': '', 'cc': 'dd'}
303-
304-
Args:
305-
convert_text (str): The input string to convert.
306-
307-
Returns:
308-
dict[str, str]: A dictionary of key-value pairs.
309-
"""
310-
return {
311-
key.strip(): (value[0].strip() if value else "")
312-
for line in text.splitlines()
313-
if line.strip()
314-
for key, *value in [line.split(":", 1)]
315-
}
316-
317-
318325
def _generate_random_string(n: int) -> str:
319326
"""
320327
Generate a random string of lowercase ASCII letters.

api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_executor.py

+74-5
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_executor_with_json_body_and_number_variable():
4848
assert executor.method == "post"
4949
assert executor.url == "https://api.example.com/data"
5050
assert executor.headers == {"Content-Type": "application/json"}
51-
assert executor.params == {}
51+
assert executor.params == []
5252
assert executor.json == {"number": 42}
5353
assert executor.data is None
5454
assert executor.files is None
@@ -101,7 +101,7 @@ def test_executor_with_json_body_and_object_variable():
101101
assert executor.method == "post"
102102
assert executor.url == "https://api.example.com/data"
103103
assert executor.headers == {"Content-Type": "application/json"}
104-
assert executor.params == {}
104+
assert executor.params == []
105105
assert executor.json == {"name": "John Doe", "age": 30, "email": "[email protected]"}
106106
assert executor.data is None
107107
assert executor.files is None
@@ -156,7 +156,7 @@ def test_executor_with_json_body_and_nested_object_variable():
156156
assert executor.method == "post"
157157
assert executor.url == "https://api.example.com/data"
158158
assert executor.headers == {"Content-Type": "application/json"}
159-
assert executor.params == {}
159+
assert executor.params == []
160160
assert executor.json == {"object": {"name": "John Doe", "age": 30, "email": "[email protected]"}}
161161
assert executor.data is None
162162
assert executor.files is None
@@ -195,7 +195,7 @@ def test_extract_selectors_from_template_with_newline():
195195
variable_pool=variable_pool,
196196
)
197197

198-
assert executor.params == {"test": "line1\nline2"}
198+
assert executor.params == [("test", "line1\nline2")]
199199

200200

201201
def test_executor_with_form_data():
@@ -244,7 +244,7 @@ def test_executor_with_form_data():
244244
assert executor.url == "https://api.example.com/upload"
245245
assert "Content-Type" in executor.headers
246246
assert "multipart/form-data" in executor.headers["Content-Type"]
247-
assert executor.params == {}
247+
assert executor.params == []
248248
assert executor.json is None
249249
assert executor.files is None
250250
assert executor.content is None
@@ -265,3 +265,72 @@ def test_executor_with_form_data():
265265
assert "Hello, World!" in raw_request
266266
assert "number_field" in raw_request
267267
assert "42" in raw_request
268+
269+
270+
def test_init_headers():
271+
def create_executor(headers: str) -> Executor:
272+
node_data = HttpRequestNodeData(
273+
title="test",
274+
method="get",
275+
url="http://example.com",
276+
headers=headers,
277+
params="",
278+
authorization=HttpRequestNodeAuthorization(type="no-auth"),
279+
)
280+
timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30)
281+
return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool())
282+
283+
executor = create_executor("aa\n cc:")
284+
executor._init_headers()
285+
assert executor.headers == {"aa": "", "cc": ""}
286+
287+
executor = create_executor("aa:bb\n cc:dd")
288+
executor._init_headers()
289+
assert executor.headers == {"aa": "bb", "cc": "dd"}
290+
291+
executor = create_executor("aa:bb\n cc:dd\n")
292+
executor._init_headers()
293+
assert executor.headers == {"aa": "bb", "cc": "dd"}
294+
295+
executor = create_executor("aa:bb\n\n cc : dd\n\n")
296+
executor._init_headers()
297+
assert executor.headers == {"aa": "bb", "cc": "dd"}
298+
299+
300+
def test_init_params():
301+
def create_executor(params: str) -> Executor:
302+
node_data = HttpRequestNodeData(
303+
title="test",
304+
method="get",
305+
url="http://example.com",
306+
headers="",
307+
params=params,
308+
authorization=HttpRequestNodeAuthorization(type="no-auth"),
309+
)
310+
timeout = HttpRequestNodeTimeout(connect=10, read=30, write=30)
311+
return Executor(node_data=node_data, timeout=timeout, variable_pool=VariablePool())
312+
313+
# Test basic key-value pairs
314+
executor = create_executor("key1:value1\nkey2:value2")
315+
executor._init_params()
316+
assert executor.params == [("key1", "value1"), ("key2", "value2")]
317+
318+
# Test empty values
319+
executor = create_executor("key1:\nkey2:")
320+
executor._init_params()
321+
assert executor.params == [("key1", ""), ("key2", "")]
322+
323+
# Test duplicate keys (which is allowed for params)
324+
executor = create_executor("key1:value1\nkey1:value2")
325+
executor._init_params()
326+
assert executor.params == [("key1", "value1"), ("key1", "value2")]
327+
328+
# Test whitespace handling
329+
executor = create_executor(" key1 : value1 \n key2 : value2 ")
330+
executor._init_params()
331+
assert executor.params == [("key1", "value1"), ("key2", "value2")]
332+
333+
# Test empty lines and extra whitespace
334+
executor = create_executor("key1:value1\n\nkey2:value2\n\n")
335+
executor._init_params()
336+
assert executor.params == [("key1", "value1"), ("key2", "value2")]

api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py

-8
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,10 @@
1414
HttpRequestNodeBody,
1515
HttpRequestNodeData,
1616
)
17-
from core.workflow.nodes.http_request.executor import _plain_text_to_dict
1817
from models.enums import UserFrom
1918
from models.workflow import WorkflowNodeExecutionStatus, WorkflowType
2019

2120

22-
def test_plain_text_to_dict():
23-
assert _plain_text_to_dict("aa\n cc:") == {"aa": "", "cc": ""}
24-
assert _plain_text_to_dict("aa:bb\n cc:dd") == {"aa": "bb", "cc": "dd"}
25-
assert _plain_text_to_dict("aa:bb\n cc:dd\n") == {"aa": "bb", "cc": "dd"}
26-
assert _plain_text_to_dict("aa:bb\n\n cc : dd\n\n") == {"aa": "bb", "cc": "dd"}
27-
28-
2921
def test_http_request_node_binary_file(monkeypatch):
3022
data = HttpRequestNodeData(
3123
title="test",

0 commit comments

Comments
 (0)