From 7120c6414c974b09453cb861d054036a6077da6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 3 Sep 2025 15:13:01 +0800 Subject: [PATCH] fix: content type of webhook (#25032) --- .../workflow/nodes/trigger_webhook/node.py | 7 +- api/services/webhook_service.py | 53 +++++++++----- .../services/test_webhook_service.py | 71 ++++++++++++++++++- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/api/core/workflow/nodes/trigger_webhook/node.py b/api/core/workflow/nodes/trigger_webhook/node.py index a18fb20c70..b26da26390 100644 --- a/api/core/workflow/nodes/trigger_webhook/node.py +++ b/api/core/workflow/nodes/trigger_webhook/node.py @@ -7,7 +7,7 @@ from core.workflow.nodes.base import BaseNode from core.workflow.nodes.base.entities import BaseNodeData, RetryConfig from core.workflow.nodes.enums import ErrorStrategy, NodeType -from .entities import WebhookData +from .entities import ContentType, WebhookData class TriggerWebhookNode(BaseNode): @@ -104,6 +104,11 @@ class TriggerWebhookNode(BaseNode): param_name = body_param.name param_type = body_param.type + if self._node_data.content_type == ContentType.TEXT: + # For text/plain, the entire body is a single string parameter + outputs[param_name] = str(webhook_data.get("body", {}).get("raw", "")) + continue + if param_type == "file": # Get File object (already processed by webhook controller) file_obj = webhook_data.get("files", {}).get(param_name) diff --git a/api/services/webhook_service.py b/api/services/webhook_service.py index f0b0ab311e..1e76308da8 100644 --- a/api/services/webhook_service.py +++ b/api/services/webhook_service.py @@ -1,3 +1,4 @@ +import json import logging from collections.abc import Mapping from typing import Any @@ -157,11 +158,21 @@ class WebhookService: "error": f"HTTP method mismatch. Expected {configured_method}, got {request_method}", } + # Validate Content-type + configured_content_type = node_data.get("content_type", "application/json").lower() + request_content_type = webhook_data["headers"].get("Content-Type", "").lower() + if not request_content_type: + request_content_type = webhook_data["headers"].get("content-type", "application/json").lower() + if configured_content_type != request_content_type: + return { + "valid": False, + "error": f"Content-type mismatch. Expected {configured_content_type}, got {request_content_type}", + } + # Validate required headers (case-insensitive) headers = node_data.get("headers", []) # Create case-insensitive header lookup webhook_headers_lower = {k.lower(): v for k, v in webhook_data["headers"].items()} - for header in headers: if header.get("required", False): header_name = header.get("name", "") @@ -175,22 +186,30 @@ class WebhookService: param_name = param.get("name", "") if param_name not in webhook_data["query_params"]: return {"valid": False, "error": f"Required query parameter missing: {param_name}"} + + if configured_content_type == "text/plain": + # For text/plain, just validate that we have a body if any body params are configured as required + body_params = node_data.get("body", []) + if body_params and any(param.get("required", False) for param in body_params): + body_data = webhook_data.get("body", {}) + raw_content = body_data.get("raw", "") + if not raw_content or not isinstance(raw_content, str): + return {"valid": False, "error": "Required body content missing for text/plain request"} + else: + body_params = node_data.get("body", []) + for body_param in body_params: + if body_param.get("required", False): + param_name = body_param.get("name", "") + param_type = body_param.get("type", "string") - # Validate required body parameters - body_params = node_data.get("body", []) - for body_param in body_params: - if body_param.get("required", False): - param_name = body_param.get("name", "") - param_type = body_param.get("type", "string") - - # Check if parameter exists - if param_type == "file": - file_obj = webhook_data.get("files", {}).get(param_name) - if not file_obj: - return {"valid": False, "error": f"Required file parameter missing: {param_name}"} - else: - if param_name not in webhook_data.get("body", {}): - return {"valid": False, "error": f"Required body parameter missing: {param_name}"} + # Check if parameter exists + if param_type == "file": + file_obj = webhook_data.get("files", {}).get(param_name) + if not file_obj: + return {"valid": False, "error": f"Required file parameter missing: {param_name}"} + else: + if param_name not in webhook_data.get("body", {}): + return {"valid": False, "error": f"Required body parameter missing: {param_name}"} return {"valid": True} @@ -253,8 +272,6 @@ class WebhookService: @classmethod def generate_webhook_response(cls, node_config: Mapping[str, Any]) -> tuple[dict[str, Any], int]: """Generate HTTP response based on node configuration.""" - import json - node_data = node_config.get("data", {}) # Get configured status code and response body diff --git a/api/tests/unit_tests/services/test_webhook_service.py b/api/tests/unit_tests/services/test_webhook_service.py index 8b58e5d76f..112b86c242 100644 --- a/api/tests/unit_tests/services/test_webhook_service.py +++ b/api/tests/unit_tests/services/test_webhook_service.py @@ -204,6 +204,75 @@ class TestWebhookServiceUnit: assert result["valid"] is False assert "Required file parameter missing: upload" in result["error"] + def test_validate_webhook_request_text_plain_with_required_body(self): + """Test webhook validation for text/plain content type with required body content.""" + # Test case 1: text/plain with raw content - should pass + webhook_data = { + "method": "POST", + "headers": {"content-type": "text/plain"}, + "query_params": {}, + "body": {"raw": "Hello World"}, + "files": {} + } + + node_config = { + "data": { + "method": "post", + "content_type": "text/plain", + "body": [{"name": "message", "type": "string", "required": True}] + } + } + + result = WebhookService.validate_webhook_request(webhook_data, node_config) + assert result["valid"] is True + + # Test case 2: text/plain without raw content but required - should fail + webhook_data_no_body = { + "method": "POST", + "headers": {"content-type": "text/plain"}, + "query_params": {}, + "body": {}, + "files": {} + } + + result = WebhookService.validate_webhook_request(webhook_data_no_body, node_config) + assert result["valid"] is False + assert "Required body content missing for text/plain request" in result["error"] + + # Test case 3: text/plain with empty raw content but required - should fail + webhook_data_empty_body = { + "method": "POST", + "headers": {"content-type": "text/plain"}, + "query_params": {}, + "body": {"raw": ""}, + "files": {} + } + + result = WebhookService.validate_webhook_request(webhook_data_empty_body, node_config) + assert result["valid"] is False + assert "Required body content missing for text/plain request" in result["error"] + + def test_validate_webhook_request_text_plain_no_body_params(self): + """Test webhook validation for text/plain content type with no body params configured.""" + webhook_data = { + "method": "POST", + "headers": {"content-type": "text/plain"}, + "query_params": {}, + "body": {"raw": "Hello World"}, + "files": {} + } + + node_config = { + "data": { + "method": "post", + "content_type": "text/plain", + "body": [] # No body params configured + } + } + + result = WebhookService.validate_webhook_request(webhook_data, node_config) + assert result["valid"] is True + def test_validate_webhook_request_validation_exception(self): """Test webhook validation with exception handling.""" webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}} @@ -214,7 +283,7 @@ class TestWebhookServiceUnit: result = WebhookService.validate_webhook_request(webhook_data, node_config) assert result["valid"] is False - assert "Validation failed:" in result["error"] + assert "Validation failed" in result["error"] def test_generate_webhook_response_default(self): """Test webhook response generation with default values."""