Skip to content

Commit

Permalink
fix: change http node params from dict to list tuple (langgenius#11665)
Browse files Browse the repository at this point in the history
  • Loading branch information
hjlarry authored Dec 15, 2024
1 parent cf0ff88 commit 9c7a1bc
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 47 deletions.
75 changes: 41 additions & 34 deletions api/core/workflow/nodes/http_request/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
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
Expand Down Expand Up @@ -67,7 +67,7 @@ def __init__(
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
Expand All @@ -89,14 +89,48 @@ def _init_url(self):
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
Expand Down Expand Up @@ -288,33 +322,6 @@ def to_log(self):
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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": "[email protected]"}
assert executor.data is None
assert executor.files is None
Expand Down Expand Up @@ -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": "[email protected]"}}
assert executor.data is None
assert executor.files is None
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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")]
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,10 @@
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",
Expand Down

0 comments on commit 9c7a1bc

Please sign in to comment.