refactor(workflow): inject http request node config through factories and defaults (#32365)

Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
-LAN-
2026-02-25 16:29:59 +08:00
committed by GitHub
parent 6f2c101e3c
commit 0964fc142e
15 changed files with 565 additions and 78 deletions

View File

@ -114,6 +114,15 @@ class MockNodeFactory(DifyNodeFactory):
code_providers=self._code_providers,
code_limits=self._code_limits,
)
elif node_type == NodeType.HTTP_REQUEST:
mock_instance = mock_class(
id=node_id,
config=node_config,
graph_init_params=self.graph_init_params,
graph_runtime_state=self.graph_runtime_state,
mock_config=self.mock_config,
http_request_config=self._http_request_config,
)
else:
mock_instance = mock_class(
id=node_id,

View File

@ -0,0 +1,33 @@
from core.workflow.nodes.http_request import build_http_request_config
def test_build_http_request_config_uses_literal_defaults():
config = build_http_request_config()
assert config.max_connect_timeout == 10
assert config.max_read_timeout == 600
assert config.max_write_timeout == 600
assert config.max_binary_size == 10 * 1024 * 1024
assert config.max_text_size == 1 * 1024 * 1024
assert config.ssl_verify is True
assert config.ssrf_default_max_retries == 3
def test_build_http_request_config_supports_explicit_overrides():
config = build_http_request_config(
max_connect_timeout=5,
max_read_timeout=30,
max_write_timeout=40,
max_binary_size=2048,
max_text_size=1024,
ssl_verify=False,
ssrf_default_max_retries=8,
)
assert config.max_connect_timeout == 5
assert config.max_read_timeout == 30
assert config.max_write_timeout == 40
assert config.max_binary_size == 2048
assert config.max_text_size == 1024
assert config.ssl_verify is False
assert config.ssrf_default_max_retries == 8

View File

@ -1,9 +1,11 @@
import pytest
from configs import dify_config
from core.workflow.nodes.http_request import (
BodyData,
HttpRequestNodeAuthorization,
HttpRequestNodeBody,
HttpRequestNodeConfig,
HttpRequestNodeData,
)
from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout
@ -12,6 +14,16 @@ from core.workflow.nodes.http_request.executor import Executor
from core.workflow.runtime import VariablePool
from core.workflow.system_variable import SystemVariable
HTTP_REQUEST_CONFIG = HttpRequestNodeConfig(
max_connect_timeout=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT,
max_read_timeout=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT,
max_write_timeout=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT,
max_binary_size=dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE,
max_text_size=dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE,
ssl_verify=dify_config.HTTP_REQUEST_NODE_SSL_VERIFY,
ssrf_default_max_retries=dify_config.SSRF_DEFAULT_MAX_RETRIES,
)
def test_executor_with_json_body_and_number_variable():
# Prepare the variable pool
@ -45,6 +57,7 @@ def test_executor_with_json_body_and_number_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -98,6 +111,7 @@ def test_executor_with_json_body_and_object_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -153,6 +167,7 @@ def test_executor_with_json_body_and_nested_object_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -196,6 +211,7 @@ def test_extract_selectors_from_template_with_newline():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -240,6 +256,7 @@ def test_executor_with_form_data():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -290,6 +307,7 @@ def test_init_headers():
return Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=VariablePool(system_variables=SystemVariable.default()),
)
@ -324,6 +342,7 @@ def test_init_params():
return Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=VariablePool(system_variables=SystemVariable.default()),
)
@ -373,6 +392,7 @@ def test_empty_api_key_raises_error_bearer():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -397,6 +417,7 @@ def test_empty_api_key_raises_error_basic():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -421,6 +442,7 @@ def test_empty_api_key_raises_error_custom():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -445,6 +467,7 @@ def test_whitespace_only_api_key_raises_error():
Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -468,6 +491,7 @@ def test_valid_api_key_works():
executor = Executor(
node_data=node_data,
timeout=timeout,
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -515,6 +539,7 @@ def test_executor_with_json_body_and_unquoted_uuid_variable():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -559,6 +584,7 @@ def test_executor_with_json_body_and_unquoted_uuid_with_newlines():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)
@ -597,6 +623,7 @@ def test_executor_with_json_body_preserves_numbers_and_strings():
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HTTP_REQUEST_CONFIG,
variable_pool=variable_pool,
)

View File

@ -0,0 +1,164 @@
import time
from typing import Any
import httpx
import pytest
from core.app.entities.app_invoke_entities import InvokeFrom
from core.workflow.entities import GraphInitParams
from core.workflow.enums import WorkflowNodeExecutionStatus
from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNode, HttpRequestNodeConfig
from core.workflow.nodes.http_request.entities import HttpRequestNodeTimeout, Response
from core.workflow.runtime import GraphRuntimeState, VariablePool
from core.workflow.system_variable import SystemVariable
from models.enums import UserFrom
HTTP_REQUEST_CONFIG = HttpRequestNodeConfig(
max_connect_timeout=10,
max_read_timeout=600,
max_write_timeout=600,
max_binary_size=10 * 1024 * 1024,
max_text_size=1 * 1024 * 1024,
ssl_verify=True,
ssrf_default_max_retries=3,
)
def test_get_default_config_without_filters_uses_literal_defaults():
default_config = HttpRequestNode.get_default_config()
timeout = default_config["config"]["timeout"]
assert default_config["type"] == "http-request"
assert timeout["connect"] == 10
assert timeout["read"] == 600
assert timeout["write"] == 600
assert timeout["max_connect_timeout"] == 10
assert timeout["max_read_timeout"] == 600
assert timeout["max_write_timeout"] == 600
assert default_config["config"]["ssl_verify"] is True
assert default_config["retry_config"]["max_retries"] == 3
def test_get_default_config_uses_injected_http_request_config():
custom_config = HttpRequestNodeConfig(
max_connect_timeout=3,
max_read_timeout=4,
max_write_timeout=5,
max_binary_size=1024,
max_text_size=2048,
ssl_verify=False,
ssrf_default_max_retries=7,
)
default_config = HttpRequestNode.get_default_config(filters={HTTP_REQUEST_CONFIG_FILTER_KEY: custom_config})
timeout = default_config["config"]["timeout"]
assert timeout["connect"] == 3
assert timeout["read"] == 4
assert timeout["write"] == 5
assert timeout["max_connect_timeout"] == 3
assert timeout["max_read_timeout"] == 4
assert timeout["max_write_timeout"] == 5
assert default_config["config"]["ssl_verify"] is False
assert default_config["retry_config"]["max_retries"] == 7
def test_get_default_config_with_malformed_http_request_config_raises_value_error():
with pytest.raises(ValueError, match="http_request_config must be an HttpRequestNodeConfig instance"):
HttpRequestNode.get_default_config(filters={HTTP_REQUEST_CONFIG_FILTER_KEY: "invalid"})
def _build_http_node(
*, timeout: dict[str, int | None] | None = None, ssl_verify: bool | None = None
) -> HttpRequestNode:
node_data: dict[str, Any] = {
"type": "http-request",
"title": "HTTP request",
"method": "get",
"url": "http://example.com",
"authorization": {"type": "no-auth"},
"headers": "",
"params": "",
"body": {"type": "none", "data": []},
}
if timeout is not None:
node_data["timeout"] = timeout
node_data["ssl_verify"] = ssl_verify
node_config: dict[str, Any] = {
"id": "http-node",
"data": node_data,
}
graph_config = {
"nodes": [
{"id": "start", "data": {"type": "start", "title": "Start"}},
node_config,
],
"edges": [],
}
graph_init_params = GraphInitParams(
tenant_id="tenant",
app_id="app",
workflow_id="workflow",
graph_config=graph_config,
user_id="user",
user_from=UserFrom.ACCOUNT,
invoke_from=InvokeFrom.DEBUGGER,
call_depth=0,
)
graph_runtime_state = GraphRuntimeState(
variable_pool=VariablePool(system_variables=SystemVariable(user_id="user", files=[]), user_inputs={}),
start_at=time.perf_counter(),
)
return HttpRequestNode(
id="http-node",
config=node_config,
graph_init_params=graph_init_params,
graph_runtime_state=graph_runtime_state,
http_request_config=HTTP_REQUEST_CONFIG,
)
def test_get_request_timeout_returns_new_object_without_mutating_node_data():
node = _build_http_node(timeout={"connect": None, "read": 30, "write": None})
original_timeout = node.node_data.timeout
assert original_timeout is not None
resolved_timeout = node._get_request_timeout(node.node_data)
assert resolved_timeout is not original_timeout
assert original_timeout.connect is None
assert original_timeout.read == 30
assert original_timeout.write is None
assert resolved_timeout == HttpRequestNodeTimeout(connect=10, read=30, write=600)
@pytest.mark.parametrize("ssl_verify", [None, False, True])
def test_run_passes_node_data_ssl_verify_to_executor(monkeypatch: pytest.MonkeyPatch, ssl_verify: bool | None):
node = _build_http_node(ssl_verify=ssl_verify)
captured: dict[str, bool | None] = {}
class FakeExecutor:
def __init__(self, *, ssl_verify: bool | None, **kwargs: Any):
captured["ssl_verify"] = ssl_verify
self.url = "http://example.com"
def to_log(self) -> str:
return "request-log"
def invoke(self) -> Response:
return Response(
httpx.Response(
status_code=200,
content=b"ok",
headers={"content-type": "text/plain"},
request=httpx.Request("GET", "http://example.com"),
)
)
monkeypatch.setattr("core.workflow.nodes.http_request.node.Executor", FakeExecutor)
result = node._run()
assert result.status == WorkflowNodeExecutionStatus.SUCCEEDED
assert captured["ssl_verify"] is ssl_verify

View File

@ -15,6 +15,7 @@ from unittest.mock import MagicMock, patch
import pytest
from core.workflow.enums import NodeType
from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNode, HttpRequestNodeConfig
from libs.datetime_utils import naive_utc_now
from models.model import App, AppMode
from models.workflow import Workflow, WorkflowType
@ -1005,13 +1006,52 @@ class TestWorkflowService:
mock_node_class = MagicMock()
mock_node_class.get_default_config.return_value = {"type": "llm", "config": {}}
mock_mapping.values.return_value = [{"latest": mock_node_class}]
mock_mapping.items.return_value = [(NodeType.LLM, {"latest": mock_node_class})]
with patch("services.workflow_service.LATEST_VERSION", "latest"):
result = workflow_service.get_default_block_configs()
assert len(result) > 0
def test_get_default_block_configs_http_request_injects_default_config(self, workflow_service):
injected_config = HttpRequestNodeConfig(
max_connect_timeout=15,
max_read_timeout=25,
max_write_timeout=35,
max_binary_size=4096,
max_text_size=2048,
ssl_verify=True,
ssrf_default_max_retries=6,
)
with (
patch("services.workflow_service.NODE_TYPE_CLASSES_MAPPING") as mock_mapping,
patch("services.workflow_service.LATEST_VERSION", "latest"),
patch(
"services.workflow_service.build_http_request_config",
return_value=injected_config,
) as mock_build_config,
):
mock_http_node_class = MagicMock()
mock_http_node_class.get_default_config.return_value = {"type": "http-request", "config": {}}
mock_llm_node_class = MagicMock()
mock_llm_node_class.get_default_config.return_value = {"type": "llm", "config": {}}
mock_mapping.items.return_value = [
(NodeType.HTTP_REQUEST, {"latest": mock_http_node_class}),
(NodeType.LLM, {"latest": mock_llm_node_class}),
]
result = workflow_service.get_default_block_configs()
assert result == [
{"type": "http-request", "config": {}},
{"type": "llm", "config": {}},
]
mock_build_config.assert_called_once()
passed_http_filters = mock_http_node_class.get_default_config.call_args.kwargs["filters"]
assert passed_http_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] is injected_config
mock_llm_node_class.get_default_config.assert_called_once_with(filters=None)
def test_get_default_block_config_for_node_type(self, workflow_service):
"""
Test get_default_block_config returns config for specific node type.
@ -1048,6 +1088,84 @@ class TestWorkflowService:
assert result == {}
def test_get_default_block_config_http_request_injects_default_config(self, workflow_service):
injected_config = HttpRequestNodeConfig(
max_connect_timeout=11,
max_read_timeout=22,
max_write_timeout=33,
max_binary_size=4096,
max_text_size=2048,
ssl_verify=False,
ssrf_default_max_retries=7,
)
with (
patch("services.workflow_service.NODE_TYPE_CLASSES_MAPPING") as mock_mapping,
patch("services.workflow_service.LATEST_VERSION", "latest"),
patch(
"services.workflow_service.build_http_request_config",
return_value=injected_config,
) as mock_build_config,
):
mock_node_class = MagicMock()
expected = {"type": "http-request", "config": {}}
mock_node_class.get_default_config.return_value = expected
mock_mapping.__contains__.return_value = True
mock_mapping.__getitem__.return_value = {"latest": mock_node_class}
result = workflow_service.get_default_block_config(NodeType.HTTP_REQUEST.value)
assert result == expected
mock_build_config.assert_called_once()
passed_filters = mock_node_class.get_default_config.call_args.kwargs["filters"]
assert passed_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] is injected_config
def test_get_default_block_config_http_request_uses_passed_config(self, workflow_service):
provided_config = HttpRequestNodeConfig(
max_connect_timeout=13,
max_read_timeout=23,
max_write_timeout=34,
max_binary_size=8192,
max_text_size=4096,
ssl_verify=True,
ssrf_default_max_retries=2,
)
with (
patch("services.workflow_service.NODE_TYPE_CLASSES_MAPPING") as mock_mapping,
patch("services.workflow_service.LATEST_VERSION", "latest"),
patch("services.workflow_service.build_http_request_config") as mock_build_config,
):
mock_node_class = MagicMock()
expected = {"type": "http-request", "config": {}}
mock_node_class.get_default_config.return_value = expected
mock_mapping.__contains__.return_value = True
mock_mapping.__getitem__.return_value = {"latest": mock_node_class}
result = workflow_service.get_default_block_config(
NodeType.HTTP_REQUEST.value,
filters={HTTP_REQUEST_CONFIG_FILTER_KEY: provided_config},
)
assert result == expected
mock_build_config.assert_not_called()
passed_filters = mock_node_class.get_default_config.call_args.kwargs["filters"]
assert passed_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] is provided_config
def test_get_default_block_config_http_request_malformed_config_raises_value_error(self, workflow_service):
with (
patch(
"services.workflow_service.NODE_TYPE_CLASSES_MAPPING",
{NodeType.HTTP_REQUEST: {"latest": HttpRequestNode}},
),
patch("services.workflow_service.LATEST_VERSION", "latest"),
):
with pytest.raises(ValueError, match="http_request_config must be an HttpRequestNodeConfig instance"):
workflow_service.get_default_block_config(
NodeType.HTTP_REQUEST.value,
filters={HTTP_REQUEST_CONFIG_FILTER_KEY: "invalid"},
)
# ==================== Workflow Conversion Tests ====================
# These tests verify converting basic apps to workflow apps