fix: normalize signed call depth for root webhook URLs

This commit is contained in:
Yanli 盐粒 2026-03-20 04:27:22 +08:00
parent 4ecba5858b
commit b8cedefd7d
3 changed files with 53 additions and 3 deletions

View File

@ -290,8 +290,10 @@ class Executor:
Reserved workflow call-depth headers are removed case-insensitively
before the canonical pair is re-added from ``workflow_call_depth``.
This keeps propagation deterministic even if a workflow author manually
configured colliding headers on the node.
The signature path mirrors Flask request matching, so URLs without an
explicit path are normalized to ``/`` before signing. This keeps
propagation deterministic even if a workflow author manually configured
colliding headers on the node.
"""
authorization = deepcopy(self.auth)
headers = deepcopy(self.headers) or {}
@ -301,12 +303,13 @@ class Executor:
}
headers = {k: v for k, v in headers.items() if k.lower() not in reserved_header_names}
parsed_url = urlparse(self.url)
signed_path = parsed_url.path or "/"
next_call_depth = str(self.workflow_call_depth + 1)
headers[WORKFLOW_CALL_DEPTH_HEADER] = next_call_depth
headers[WORKFLOW_CALL_DEPTH_SIGNATURE_HEADER] = build_workflow_call_depth_signature(
secret_key=self._http_request_config.secret_key,
method=self.method,
path=parsed_url.path,
path=signed_path,
depth=next_call_depth,
)

View File

@ -100,6 +100,7 @@ def test_run_uses_single_node_execution_branch(
workflow=workflow,
single_iteration_run=single_iteration_run,
single_loop_run=single_loop_run,
call_depth=0,
)
init_graph.assert_not_called()
@ -156,6 +157,7 @@ def test_single_node_run_validates_target_node_config(monkeypatch) -> None:
node_id="loop-node",
user_inputs={},
graph_runtime_state=graph_runtime_state,
call_depth=0,
node_type_filter_key="loop_id",
node_type_label="loop",
)

View File

@ -937,3 +937,48 @@ def test_executor_propagates_workflow_call_depth_to_arbitrary_target_with_secret
path="/data",
depth="3",
)
def test_executor_normalizes_empty_url_path_when_signing_workflow_call_depth():
variable_pool = VariablePool(
system_variables=SystemVariable.default(),
user_inputs={},
)
node_data = HttpRequestNodeData(
title="External target without explicit path",
method="get",
url="https://api.example.com",
authorization=HttpRequestNodeAuthorization(type="no-auth"),
headers="X-Test: value",
params="",
)
executor = Executor(
node_data=node_data,
timeout=HttpRequestNodeTimeout(connect=10, read=30, write=30),
http_request_config=HttpRequestNodeConfig(
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,
max_binary_size=HTTP_REQUEST_CONFIG.max_binary_size,
max_text_size=HTTP_REQUEST_CONFIG.max_text_size,
ssl_verify=HTTP_REQUEST_CONFIG.ssl_verify,
ssrf_default_max_retries=HTTP_REQUEST_CONFIG.ssrf_default_max_retries,
secret_key=TEST_SECRET_KEY,
),
variable_pool=variable_pool,
workflow_call_depth=2,
http_client=ssrf_proxy,
file_manager=file_manager,
)
headers = executor._assembling_headers()
assert headers["X-Test"] == "value"
assert headers[WORKFLOW_CALL_DEPTH_HEADER] == "3"
assert headers[WORKFLOW_CALL_DEPTH_SIGNATURE_HEADER] == build_workflow_call_depth_signature(
secret_key=TEST_SECRET_KEY,
method="get",
path="/",
depth="3",
)