From 0964fc142e2141fe092851f47b14d04cefe79504 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Wed, 25 Feb 2026 16:29:59 +0800 Subject: [PATCH] refactor(workflow): inject http request node config through factories and defaults (#32365) Signed-off-by: -LAN- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/.importlinter | 3 - api/core/app/workflow/node_factory.py | 57 +++--- .../workflow/nodes/http_request/__init__.py | 22 ++- .../workflow/nodes/http_request/config.py | 33 ++++ .../workflow/nodes/http_request/entities.py | 30 +++- .../workflow/nodes/http_request/executor.py | 21 ++- api/core/workflow/nodes/http_request/node.py | 46 ++--- api/services/rag_pipeline/rag_pipeline.py | 31 +++- api/services/workflow_service.py | 31 +++- .../workflow/nodes/test_http.py | 16 +- .../graph_engine/test_mock_factory.py | 9 + .../nodes/http_request/test_config.py | 33 ++++ .../test_http_request_executor.py | 27 +++ .../http_request/test_http_request_node.py | 164 ++++++++++++++++++ .../services/test_workflow_service.py | 120 ++++++++++++- 15 files changed, 565 insertions(+), 78 deletions(-) create mode 100644 api/core/workflow/nodes/http_request/config.py create mode 100644 api/tests/unit_tests/core/workflow/nodes/http_request/test_config.py create mode 100644 api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py diff --git a/api/.importlinter b/api/.importlinter index b9d688c1fa..ee9b8464a4 100644 --- a/api/.importlinter +++ b/api/.importlinter @@ -115,9 +115,6 @@ ignore_imports = core.workflow.nodes.datasource.datasource_node -> models.tools core.workflow.nodes.datasource.datasource_node -> services.datasource_provider_service core.workflow.nodes.document_extractor.node -> core.helper.ssrf_proxy - core.workflow.nodes.http_request.entities -> configs - core.workflow.nodes.http_request.executor -> configs - core.workflow.nodes.http_request.node -> configs core.workflow.nodes.http_request.node -> core.tools.tool_file_manager core.workflow.nodes.iteration.iteration_node -> core.app.workflow.node_factory core.workflow.nodes.knowledge_index.knowledge_index_node -> core.rag.index_processor.index_processor_factory diff --git a/api/core/app/workflow/node_factory.py b/api/core/app/workflow/node_factory.py index efb2a74176..bc4470cd50 100644 --- a/api/core/app/workflow/node_factory.py +++ b/api/core/app/workflow/node_factory.py @@ -1,4 +1,3 @@ -from collections.abc import Callable, Sequence from typing import TYPE_CHECKING, final from typing_extensions import override @@ -17,14 +16,10 @@ from core.workflow.nodes.base.node import Node from core.workflow.nodes.code.code_node import CodeNode from core.workflow.nodes.code.limits import CodeNodeLimits from core.workflow.nodes.document_extractor import DocumentExtractorNode, UnstructuredApiConfig -from core.workflow.nodes.http_request.node import HttpRequestNode +from core.workflow.nodes.http_request import HttpRequestNode, build_http_request_config from core.workflow.nodes.knowledge_retrieval.knowledge_retrieval_node import KnowledgeRetrievalNode from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING -from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol -from core.workflow.nodes.template_transform.template_renderer import ( - CodeExecutorJinja2TemplateRenderer, - Jinja2TemplateRenderer, -) +from core.workflow.nodes.template_transform.template_renderer import CodeExecutorJinja2TemplateRenderer from core.workflow.nodes.template_transform.template_transform_node import TemplateTransformNode if TYPE_CHECKING: @@ -45,23 +40,12 @@ class DifyNodeFactory(NodeFactory): self, graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", - code_executor: type[CodeExecutor] | None = None, - code_providers: Sequence[type[CodeNodeProvider]] | None = None, - code_limits: CodeNodeLimits | None = None, - template_renderer: Jinja2TemplateRenderer | None = None, - template_transform_max_output_length: int | None = None, - http_request_http_client: HttpClientProtocol | None = None, - http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager, - http_request_file_manager: FileManagerProtocol | None = None, - document_extractor_unstructured_api_config: UnstructuredApiConfig | None = None, ) -> None: self.graph_init_params = graph_init_params self.graph_runtime_state = graph_runtime_state - self._code_executor: type[CodeExecutor] = code_executor or CodeExecutor - self._code_providers: tuple[type[CodeNodeProvider], ...] = ( - tuple(code_providers) if code_providers else CodeNode.default_code_providers() - ) - self._code_limits = code_limits or CodeNodeLimits( + self._code_executor: type[CodeExecutor] = CodeExecutor + self._code_providers: tuple[type[CodeNodeProvider], ...] = CodeNode.default_code_providers() + self._code_limits = CodeNodeLimits( max_string_length=dify_config.CODE_MAX_STRING_LENGTH, max_number=dify_config.CODE_MAX_NUMBER, min_number=dify_config.CODE_MIN_NUMBER, @@ -71,20 +55,24 @@ class DifyNodeFactory(NodeFactory): max_string_array_length=dify_config.CODE_MAX_STRING_ARRAY_LENGTH, max_object_array_length=dify_config.CODE_MAX_OBJECT_ARRAY_LENGTH, ) - self._template_renderer = template_renderer or CodeExecutorJinja2TemplateRenderer() - self._template_transform_max_output_length = ( - template_transform_max_output_length or dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH - ) - self._http_request_http_client = http_request_http_client or ssrf_proxy - self._http_request_tool_file_manager_factory = http_request_tool_file_manager_factory - self._http_request_file_manager = http_request_file_manager or file_manager + self._template_renderer = CodeExecutorJinja2TemplateRenderer() + self._template_transform_max_output_length = dify_config.TEMPLATE_TRANSFORM_MAX_LENGTH + self._http_request_http_client = ssrf_proxy + self._http_request_tool_file_manager_factory = ToolFileManager + self._http_request_file_manager = file_manager self._rag_retrieval = DatasetRetrieval() - self._document_extractor_unstructured_api_config = ( - document_extractor_unstructured_api_config - or UnstructuredApiConfig( - api_url=dify_config.UNSTRUCTURED_API_URL, - api_key=dify_config.UNSTRUCTURED_API_KEY or "", - ) + self._document_extractor_unstructured_api_config = UnstructuredApiConfig( + api_url=dify_config.UNSTRUCTURED_API_URL, + api_key=dify_config.UNSTRUCTURED_API_KEY or "", + ) + self._http_request_config = build_http_request_config( + 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, ) @override @@ -146,6 +134,7 @@ class DifyNodeFactory(NodeFactory): config=node_config, graph_init_params=self.graph_init_params, graph_runtime_state=self.graph_runtime_state, + http_request_config=self._http_request_config, http_client=self._http_request_http_client, tool_file_manager_factory=self._http_request_tool_file_manager_factory, file_manager=self._http_request_file_manager, diff --git a/api/core/workflow/nodes/http_request/__init__.py b/api/core/workflow/nodes/http_request/__init__.py index c51c678999..b29099db23 100644 --- a/api/core/workflow/nodes/http_request/__init__.py +++ b/api/core/workflow/nodes/http_request/__init__.py @@ -1,4 +1,22 @@ -from .entities import BodyData, HttpRequestNodeAuthorization, HttpRequestNodeBody, HttpRequestNodeData +from .config import build_http_request_config, resolve_http_request_config +from .entities import ( + HTTP_REQUEST_CONFIG_FILTER_KEY, + BodyData, + HttpRequestNodeAuthorization, + HttpRequestNodeBody, + HttpRequestNodeConfig, + HttpRequestNodeData, +) from .node import HttpRequestNode -__all__ = ["BodyData", "HttpRequestNode", "HttpRequestNodeAuthorization", "HttpRequestNodeBody", "HttpRequestNodeData"] +__all__ = [ + "HTTP_REQUEST_CONFIG_FILTER_KEY", + "BodyData", + "HttpRequestNode", + "HttpRequestNodeAuthorization", + "HttpRequestNodeBody", + "HttpRequestNodeConfig", + "HttpRequestNodeData", + "build_http_request_config", + "resolve_http_request_config", +] diff --git a/api/core/workflow/nodes/http_request/config.py b/api/core/workflow/nodes/http_request/config.py new file mode 100644 index 0000000000..53bf6c7ae4 --- /dev/null +++ b/api/core/workflow/nodes/http_request/config.py @@ -0,0 +1,33 @@ +from collections.abc import Mapping + +from .entities import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNodeConfig + + +def build_http_request_config( + *, + max_connect_timeout: int = 10, + max_read_timeout: int = 600, + max_write_timeout: int = 600, + max_binary_size: int = 10 * 1024 * 1024, + max_text_size: int = 1 * 1024 * 1024, + ssl_verify: bool = True, + ssrf_default_max_retries: int = 3, +) -> HttpRequestNodeConfig: + return HttpRequestNodeConfig( + max_connect_timeout=max_connect_timeout, + max_read_timeout=max_read_timeout, + max_write_timeout=max_write_timeout, + max_binary_size=max_binary_size, + max_text_size=max_text_size, + ssl_verify=ssl_verify, + ssrf_default_max_retries=ssrf_default_max_retries, + ) + + +def resolve_http_request_config(filters: Mapping[str, object] | None) -> HttpRequestNodeConfig: + if not filters: + raise ValueError("http_request_config is required to build HTTP request default config") + config = filters.get(HTTP_REQUEST_CONFIG_FILTER_KEY) + if not isinstance(config, HttpRequestNodeConfig): + raise ValueError("http_request_config must be an HttpRequestNodeConfig instance") + return config diff --git a/api/core/workflow/nodes/http_request/entities.py b/api/core/workflow/nodes/http_request/entities.py index e323533835..0eda20f485 100644 --- a/api/core/workflow/nodes/http_request/entities.py +++ b/api/core/workflow/nodes/http_request/entities.py @@ -1,5 +1,6 @@ import mimetypes from collections.abc import Sequence +from dataclasses import dataclass from email.message import Message from typing import Any, Literal @@ -7,9 +8,10 @@ import charset_normalizer import httpx from pydantic import BaseModel, Field, ValidationInfo, field_validator -from configs import dify_config from core.workflow.nodes.base import BaseNodeData +HTTP_REQUEST_CONFIG_FILTER_KEY = "http_request_config" + class HttpRequestNodeAuthorizationConfig(BaseModel): type: Literal["basic", "bearer", "custom"] @@ -59,9 +61,27 @@ class HttpRequestNodeBody(BaseModel): class HttpRequestNodeTimeout(BaseModel): - connect: int = dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT - read: int = dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT - write: int = dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT + connect: int | None = None + read: int | None = None + write: int | None = None + + +@dataclass(frozen=True, slots=True) +class HttpRequestNodeConfig: + max_connect_timeout: int + max_read_timeout: int + max_write_timeout: int + max_binary_size: int + max_text_size: int + ssl_verify: bool + ssrf_default_max_retries: int + + def default_timeout(self) -> "HttpRequestNodeTimeout": + return HttpRequestNodeTimeout( + connect=self.max_connect_timeout, + read=self.max_read_timeout, + write=self.max_write_timeout, + ) class HttpRequestNodeData(BaseNodeData): @@ -91,7 +111,7 @@ class HttpRequestNodeData(BaseNodeData): params: str body: HttpRequestNodeBody | None = None timeout: HttpRequestNodeTimeout | None = None - ssl_verify: bool | None = dify_config.HTTP_REQUEST_NODE_SSL_VERIFY + ssl_verify: bool | None = None class Response: diff --git a/api/core/workflow/nodes/http_request/executor.py b/api/core/workflow/nodes/http_request/executor.py index d067e38728..8f180b47b5 100644 --- a/api/core/workflow/nodes/http_request/executor.py +++ b/api/core/workflow/nodes/http_request/executor.py @@ -10,7 +10,6 @@ from urllib.parse import urlencode, urlparse import httpx from json_repair import repair_json -from configs import dify_config from core.helper.ssrf_proxy import ssrf_proxy from core.variables.segments import ArrayFileSegment, FileSegment from core.workflow.file.enums import FileTransferMethod @@ -20,6 +19,7 @@ from core.workflow.runtime import VariablePool from ..protocols import FileManagerProtocol, HttpClientProtocol from .entities import ( HttpRequestNodeAuthorization, + HttpRequestNodeConfig, HttpRequestNodeData, HttpRequestNodeTimeout, Response, @@ -78,10 +78,13 @@ class Executor: node_data: HttpRequestNodeData, timeout: HttpRequestNodeTimeout, variable_pool: VariablePool, - max_retries: int = dify_config.SSRF_DEFAULT_MAX_RETRIES, + http_request_config: HttpRequestNodeConfig, + max_retries: int | None = None, + ssl_verify: bool | None = None, http_client: HttpClientProtocol | None = None, file_manager: FileManagerProtocol | None = None, ): + self._http_request_config = http_request_config # If authorization API key is present, convert the API key using the variable pool if node_data.authorization.type == "api-key": if node_data.authorization.config is None: @@ -99,14 +102,20 @@ class Executor: self.method = node_data.method self.auth = node_data.authorization self.timeout = timeout - self.ssl_verify = node_data.ssl_verify + self.ssl_verify = ssl_verify if ssl_verify is not None else node_data.ssl_verify + if self.ssl_verify is None: + self.ssl_verify = self._http_request_config.ssl_verify + if not isinstance(self.ssl_verify, bool): + raise ValueError("ssl_verify must be a boolean") self.params = None self.headers = {} self.content = None self.files = None self.data = None self.json = None - self.max_retries = max_retries + self.max_retries = ( + max_retries if max_retries is not None else self._http_request_config.ssrf_default_max_retries + ) self._http_client = http_client or ssrf_proxy self._file_manager = file_manager or default_file_manager @@ -319,9 +328,9 @@ class Executor: executor_response = Response(response) threshold_size = ( - dify_config.HTTP_REQUEST_NODE_MAX_BINARY_SIZE + self._http_request_config.max_binary_size if executor_response.is_file - else dify_config.HTTP_REQUEST_NODE_MAX_TEXT_SIZE + else self._http_request_config.max_text_size ) if executor_response.size > threshold_size: raise ResponseSizeError( diff --git a/api/core/workflow/nodes/http_request/node.py b/api/core/workflow/nodes/http_request/node.py index c9aca1b992..d45775652f 100644 --- a/api/core/workflow/nodes/http_request/node.py +++ b/api/core/workflow/nodes/http_request/node.py @@ -3,7 +3,6 @@ import mimetypes from collections.abc import Callable, Mapping, Sequence from typing import TYPE_CHECKING, Any -from configs import dify_config from core.helper.ssrf_proxy import ssrf_proxy from core.tools.tool_file_manager import ToolFileManager from core.variables.segments import ArrayFileSegment @@ -18,19 +17,16 @@ from core.workflow.nodes.http_request.executor import Executor from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtocol from factories import file_factory +from .config import build_http_request_config, resolve_http_request_config from .entities import ( + HTTP_REQUEST_CONFIG_FILTER_KEY, + HttpRequestNodeConfig, HttpRequestNodeData, HttpRequestNodeTimeout, Response, ) from .exc import HttpRequestNodeError, RequestBodyError -HTTP_REQUEST_DEFAULT_TIMEOUT = HttpRequestNodeTimeout( - connect=dify_config.HTTP_REQUEST_MAX_CONNECT_TIMEOUT, - read=dify_config.HTTP_REQUEST_MAX_READ_TIMEOUT, - write=dify_config.HTTP_REQUEST_MAX_WRITE_TIMEOUT, -) - logger = logging.getLogger(__name__) if TYPE_CHECKING: @@ -48,6 +44,7 @@ class HttpRequestNode(Node[HttpRequestNodeData]): graph_init_params: "GraphInitParams", graph_runtime_state: "GraphRuntimeState", *, + http_request_config: HttpRequestNodeConfig, http_client: HttpClientProtocol | None = None, tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager, file_manager: FileManagerProtocol | None = None, @@ -58,12 +55,18 @@ class HttpRequestNode(Node[HttpRequestNodeData]): graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, ) + self._http_request_config = http_request_config self._http_client = http_client or ssrf_proxy self._tool_file_manager_factory = tool_file_manager_factory self._file_manager = file_manager or default_file_manager @classmethod def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]: + if not filters or HTTP_REQUEST_CONFIG_FILTER_KEY not in filters: + http_request_config = build_http_request_config() + else: + http_request_config = resolve_http_request_config(filters) + default_timeout = http_request_config.default_timeout() return { "type": "http-request", "config": { @@ -73,15 +76,15 @@ class HttpRequestNode(Node[HttpRequestNodeData]): }, "body": {"type": "none"}, "timeout": { - **HTTP_REQUEST_DEFAULT_TIMEOUT.model_dump(), - "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, + **default_timeout.model_dump(), + "max_connect_timeout": http_request_config.max_connect_timeout, + "max_read_timeout": http_request_config.max_read_timeout, + "max_write_timeout": http_request_config.max_write_timeout, }, - "ssl_verify": dify_config.HTTP_REQUEST_NODE_SSL_VERIFY, + "ssl_verify": http_request_config.ssl_verify, }, "retry_config": { - "max_retries": dify_config.SSRF_DEFAULT_MAX_RETRIES, + "max_retries": http_request_config.ssrf_default_max_retries, "retry_interval": 0.5 * (2**2), "retry_enabled": True, }, @@ -98,7 +101,9 @@ class HttpRequestNode(Node[HttpRequestNodeData]): node_data=self.node_data, timeout=self._get_request_timeout(self.node_data), variable_pool=self.graph_runtime_state.variable_pool, + http_request_config=self._http_request_config, max_retries=0, + ssl_verify=self.node_data.ssl_verify, http_client=self._http_client, file_manager=self._file_manager, ) @@ -142,16 +147,17 @@ class HttpRequestNode(Node[HttpRequestNodeData]): error_type=type(e).__name__, ) - @staticmethod - def _get_request_timeout(node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout: + def _get_request_timeout(self, node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout: + default_timeout = self._http_request_config.default_timeout() timeout = node_data.timeout if timeout is None: - return HTTP_REQUEST_DEFAULT_TIMEOUT + return default_timeout - timeout.connect = timeout.connect or HTTP_REQUEST_DEFAULT_TIMEOUT.connect - timeout.read = timeout.read or HTTP_REQUEST_DEFAULT_TIMEOUT.read - timeout.write = timeout.write or HTTP_REQUEST_DEFAULT_TIMEOUT.write - return timeout + return HttpRequestNodeTimeout( + connect=timeout.connect or default_timeout.connect, + read=timeout.read or default_timeout.read, + write=timeout.write or default_timeout.write, + ) @classmethod def _extract_variable_selector_to_variable_mapping( diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index 4e33b312f4..4ae3496cd6 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -47,6 +47,7 @@ from core.workflow.graph_events import NodeRunFailedEvent, NodeRunSucceededEvent from core.workflow.graph_events.base import GraphNodeEventBase from core.workflow.node_events.base import NodeRunResult from core.workflow.nodes.base.node import Node +from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING from core.workflow.repositories.workflow_node_execution_repository import OrderConfig from core.workflow.runtime import VariablePool @@ -380,9 +381,22 @@ class RagPipelineService: """ # return default block config default_block_configs: list[dict[str, Any]] = [] - for node_class_mapping in NODE_TYPE_CLASSES_MAPPING.values(): + for node_type, node_class_mapping in NODE_TYPE_CLASSES_MAPPING.items(): node_class = node_class_mapping[LATEST_VERSION] - default_config = node_class.get_default_config() + filters = None + if node_type is NodeType.HTTP_REQUEST: + filters = { + HTTP_REQUEST_CONFIG_FILTER_KEY: build_http_request_config( + 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, + ) + } + default_config = node_class.get_default_config(filters=filters) if default_config: default_block_configs.append(dict(default_config)) @@ -402,7 +416,18 @@ class RagPipelineService: return None node_class = NODE_TYPE_CLASSES_MAPPING[node_type_enum][LATEST_VERSION] - default_config = node_class.get_default_config(filters=filters) + final_filters = dict(filters) if filters else {} + if node_type_enum is NodeType.HTTP_REQUEST and HTTP_REQUEST_CONFIG_FILTER_KEY not in final_filters: + final_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] = build_http_request_config( + 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, + ) + default_config = node_class.get_default_config(filters=final_filters or None) if not default_config: return None diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index cff334a44a..abcd41b1be 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -26,6 +26,7 @@ from core.workflow.graph_events import GraphNodeEventBase, NodeRunFailedEvent, N from core.workflow.node_events import NodeRunResult from core.workflow.nodes import NodeType from core.workflow.nodes.base.node import Node +from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, build_http_request_config from core.workflow.nodes.human_input.entities import ( DeliveryChannelConfig, HumanInputNodeData, @@ -618,9 +619,22 @@ class WorkflowService: """ # return default block config default_block_configs: list[Mapping[str, object]] = [] - for node_class_mapping in NODE_TYPE_CLASSES_MAPPING.values(): + for node_type, node_class_mapping in NODE_TYPE_CLASSES_MAPPING.items(): node_class = node_class_mapping[LATEST_VERSION] - default_config = node_class.get_default_config() + filters = None + if node_type is NodeType.HTTP_REQUEST: + filters = { + HTTP_REQUEST_CONFIG_FILTER_KEY: build_http_request_config( + 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, + ) + } + default_config = node_class.get_default_config(filters=filters) if default_config: default_block_configs.append(default_config) @@ -642,7 +656,18 @@ class WorkflowService: return {} node_class = NODE_TYPE_CLASSES_MAPPING[node_type_enum][LATEST_VERSION] - default_config = node_class.get_default_config(filters=filters) + resolved_filters = dict(filters) if filters else {} + if node_type_enum is NodeType.HTTP_REQUEST and HTTP_REQUEST_CONFIG_FILTER_KEY not in resolved_filters: + resolved_filters[HTTP_REQUEST_CONFIG_FILTER_KEY] = build_http_request_config( + 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, + ) + default_config = node_class.get_default_config(filters=resolved_filters or None) if not default_config: return {} diff --git a/api/tests/integration_tests/workflow/nodes/test_http.py b/api/tests/integration_tests/workflow/nodes/test_http.py index 1bcac3b5fe..0473d9832a 100644 --- a/api/tests/integration_tests/workflow/nodes/test_http.py +++ b/api/tests/integration_tests/workflow/nodes/test_http.py @@ -4,17 +4,28 @@ from urllib.parse import urlencode import pytest +from configs import dify_config from core.app.entities.app_invoke_entities import InvokeFrom from core.app.workflow.node_factory import DifyNodeFactory from core.workflow.entities import GraphInitParams from core.workflow.enums import WorkflowNodeExecutionStatus from core.workflow.graph import Graph -from core.workflow.nodes.http_request.node import HttpRequestNode +from core.workflow.nodes.http_request import HttpRequestNode, HttpRequestNodeConfig from core.workflow.runtime import GraphRuntimeState, VariablePool from core.workflow.system_variable import SystemVariable from models.enums import UserFrom from tests.integration_tests.workflow.nodes.__mock.http import setup_http_mock +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 init_http_node(config: dict): graph_config = { @@ -64,6 +75,7 @@ def init_http_node(config: dict): config=config, graph_init_params=init_params, graph_runtime_state=graph_runtime_state, + http_request_config=HTTP_REQUEST_CONFIG, ) return node @@ -215,6 +227,7 @@ def test_custom_auth_with_empty_api_key_raises_error(setup_http_mock): Executor( node_data=node_data, timeout=HttpRequestNodeTimeout(connect=10, read=30, write=10), + http_request_config=HTTP_REQUEST_CONFIG, variable_pool=variable_pool, ) @@ -702,6 +715,7 @@ def test_nested_object_variable_selector(setup_http_mock): config=graph_config["nodes"][1], graph_init_params=init_params, graph_runtime_state=graph_runtime_state, + http_request_config=HTTP_REQUEST_CONFIG, ) result = node._run() diff --git a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py index 170445225b..8c58fe1922 100644 --- a/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py +++ b/api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py @@ -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, diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_config.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_config.py new file mode 100644 index 0000000000..90f4cd018b --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_config.py @@ -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 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 cefc4967ac..65f4de8c1d 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 @@ -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, ) diff --git a/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py new file mode 100644 index 0000000000..472718188f --- /dev/null +++ b/api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py @@ -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 diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index ae5b194afb..3a4f2d392a 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -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