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..c1faeb6a6c 100644 --- a/api/core/app/workflow/node_factory.py +++ b/api/core/app/workflow/node_factory.py @@ -17,7 +17,7 @@ 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, HttpRequestNodeConfig 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 @@ -45,6 +45,7 @@ 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, @@ -54,6 +55,7 @@ class DifyNodeFactory(NodeFactory): http_request_tool_file_manager_factory: Callable[[], ToolFileManager] = ToolFileManager, http_request_file_manager: FileManagerProtocol | None = None, document_extractor_unstructured_api_config: UnstructuredApiConfig | None = None, + http_request_config: HttpRequestNodeConfig | None = None, ) -> None: self.graph_init_params = graph_init_params self.graph_runtime_state = graph_runtime_state @@ -86,6 +88,15 @@ class DifyNodeFactory(NodeFactory): api_key=dify_config.UNSTRUCTURED_API_KEY or "", ) ) + self._http_request_config = http_request_config or 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, + ) @override def create_node(self, node_config: NodeConfigDict) -> Node: @@ -146,6 +157,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..92b23310d2 100644 --- a/api/core/workflow/nodes/http_request/__init__.py +++ b/api/core/workflow/nodes/http_request/__init__.py @@ -1,4 +1,19 @@ -from .entities import BodyData, HttpRequestNodeAuthorization, HttpRequestNodeBody, HttpRequestNodeData +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", +] 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 1e6e14482b..be4b689ab5 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,21 @@ class Executor: self.method = node_data.method self.auth = node_data.authorization self.timeout = timeout - self.ssl_verify = node_data.ssl_verify + resolved_ssl_verify = ssl_verify if ssl_verify is not None else node_data.ssl_verify + if resolved_ssl_verify is None: + resolved_ssl_verify = self._http_request_config.ssl_verify + if not isinstance(resolved_ssl_verify, bool): + raise ValueError("ssl_verify must be a boolean") + self.ssl_verify = resolved_ssl_verify 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 +329,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..9f3b4ba74c 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 @@ -19,18 +18,14 @@ from core.workflow.nodes.protocols import FileManagerProtocol, HttpClientProtoco from factories import file_factory 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: @@ -38,6 +33,15 @@ if TYPE_CHECKING: from core.workflow.runtime import GraphRuntimeState +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 + + class HttpRequestNode(Node[HttpRequestNodeData]): node_type = NodeType.HTTP_REQUEST @@ -48,6 +52,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 +63,15 @@ 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]: + http_request_config = _resolve_http_request_config(filters) + default_timeout = http_request_config.default_timeout() return { "type": "http-request", "config": { @@ -73,15 +81,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 +106,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._resolve_ssl_verify(self.node_data), http_client=self._http_client, file_manager=self._file_manager, ) @@ -142,15 +152,18 @@ class HttpRequestNode(Node[HttpRequestNodeData]): error_type=type(e).__name__, ) - @staticmethod - def _get_request_timeout(node_data: HttpRequestNodeData) -> HttpRequestNodeTimeout: + def _resolve_ssl_verify(self, node_data: HttpRequestNodeData) -> bool: + return self._http_request_config.ssl_verify if node_data.ssl_verify is None else node_data.ssl_verify + + 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 + timeout.connect = timeout.connect or default_timeout.connect + timeout.read = timeout.read or default_timeout.read + timeout.write = timeout.write or default_timeout.write return timeout @classmethod diff --git a/api/services/rag_pipeline/rag_pipeline.py b/api/services/rag_pipeline/rag_pipeline.py index 4e33b312f4..a523c6bd7a 100644 --- a/api/services/rag_pipeline/rag_pipeline.py +++ b/api/services/rag_pipeline/rag_pipeline.py @@ -42,6 +42,7 @@ from core.workflow.entities.workflow_node_execution import ( WorkflowNodeExecutionStatus, ) from core.workflow.enums import ErrorStrategy, NodeType, SystemVariableKey +from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNodeConfig from core.workflow.errors import WorkflowNodeRunFailedError from core.workflow.graph_events import NodeRunFailedEvent, NodeRunSucceededEvent from core.workflow.graph_events.base import GraphNodeEventBase @@ -86,6 +87,18 @@ from services.workflow_draft_variable_service import DraftVariableSaver, DraftVa logger = logging.getLogger(__name__) +def _build_http_request_config() -> HttpRequestNodeConfig: + return 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, + ) + + class RagPipelineService: def __init__(self, session_maker: sessionmaker | None = None): """Initialize RagPipelineService with repository dependencies.""" @@ -380,9 +393,12 @@ 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()} + default_config = node_class.get_default_config(filters=filters) if default_config: default_block_configs.append(dict(default_config)) @@ -402,7 +418,10 @@ class RagPipelineService: return None 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() + default_config = node_class.get_default_config(filters=resolved_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..0e6c997aee 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -34,6 +34,7 @@ from core.workflow.nodes.human_input.entities import ( ) from core.workflow.nodes.human_input.enums import HumanInputFormKind from core.workflow.nodes.human_input.human_input_node import HumanInputNode +from core.workflow.nodes.http_request import HTTP_REQUEST_CONFIG_FILTER_KEY, HttpRequestNodeConfig from core.workflow.nodes.node_mapping import LATEST_VERSION, NODE_TYPE_CLASSES_MAPPING from core.workflow.nodes.start.entities import StartNodeData from core.workflow.repositories.human_input_form_repository import FormCreateParams @@ -70,6 +71,18 @@ from .human_input_delivery_test_service import ( from .workflow_draft_variable_service import DraftVariableSaver, DraftVarLoader, WorkflowDraftVariableService +def _build_http_request_config() -> HttpRequestNodeConfig: + return 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, + ) + + class WorkflowService: """ Workflow Service @@ -618,9 +631,12 @@ 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()} + default_config = node_class.get_default_config(filters=filters) if default_config: default_block_configs.append(default_config) @@ -642,7 +658,10 @@ 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() + 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_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/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index ae5b194afb..1d57ec59dd 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -1005,7 +1005,7 @@ 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()