mirror of
https://github.com/langgenius/dify.git
synced 2026-03-10 11:10:19 +08:00
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:
parent
6f2c101e3c
commit
0964fc142e
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
]
|
||||
|
||||
33
api/core/workflow/nodes/http_request/config.py
Normal file
33
api/core/workflow/nodes/http_request/config.py
Normal file
@ -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
|
||||
@ -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:
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user