mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 22:47:15 +08:00
query param of webhook backend support
This commit is contained in:
parent
32f4d1af8b
commit
080cdda4fa
@ -5,7 +5,7 @@ Trigger Manager for loading and managing trigger providers and triggers
|
|||||||
import logging
|
import logging
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
from flask import Request
|
from flask import Request
|
||||||
|
|
||||||
|
|||||||
@ -90,7 +90,7 @@ class WebhookService:
|
|||||||
data = {
|
data = {
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
"headers": dict(request.headers),
|
"headers": dict(request.headers),
|
||||||
"query_params": dict(request.args),
|
"query_params": cls._extract_query_params(),
|
||||||
"body": {},
|
"body": {},
|
||||||
"files": {},
|
"files": {},
|
||||||
}
|
}
|
||||||
@ -120,6 +120,43 @@ class WebhookService:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _extract_query_params(cls) -> dict[str, Any]:
|
||||||
|
"""Extract query parameters preserving multi-value entries."""
|
||||||
|
if not request.args:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
query_params: dict[str, Any] = {}
|
||||||
|
for key, value in request.args.items():
|
||||||
|
query_params[key] = cls._convert_query_param_value(value)
|
||||||
|
|
||||||
|
return query_params
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _convert_query_param_value(cls, value: str) -> Any:
|
||||||
|
"""Convert query parameter strings to numbers or booleans when applicable."""
|
||||||
|
lower_value = value.lower()
|
||||||
|
bool_map = {
|
||||||
|
"true": True,
|
||||||
|
"false": False,
|
||||||
|
"yes": True,
|
||||||
|
"no": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if lower_value in bool_map:
|
||||||
|
return bool_map[lower_value]
|
||||||
|
|
||||||
|
if cls._can_convert_to_number(value):
|
||||||
|
try:
|
||||||
|
numeric_value = float(value)
|
||||||
|
if numeric_value.is_integer():
|
||||||
|
return int(numeric_value)
|
||||||
|
return numeric_value
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _validate_content_length(cls) -> None:
|
def _validate_content_length(cls) -> None:
|
||||||
"""Validate request content length against maximum allowed size."""
|
"""Validate request content length against maximum allowed size."""
|
||||||
@ -285,11 +322,24 @@ class WebhookService:
|
|||||||
return cls._validation_error(f"Required header missing: {header_name}")
|
return cls._validation_error(f"Required header missing: {header_name}")
|
||||||
|
|
||||||
# Validate required query parameters
|
# Validate required query parameters
|
||||||
|
query_params = webhook_data.get("query_params", {})
|
||||||
for param in node_data.get("params", []):
|
for param in node_data.get("params", []):
|
||||||
if param.get("required", False):
|
param_name = param.get("name", "")
|
||||||
param_name = param.get("name", "")
|
param_type = param.get("type", SegmentType.STRING)
|
||||||
if param_name not in webhook_data["query_params"]:
|
is_required = param.get("required", False)
|
||||||
return cls._validation_error(f"Required query parameter missing: {param_name}")
|
|
||||||
|
param_exists = param_name in query_params
|
||||||
|
if is_required and not param_exists:
|
||||||
|
return cls._validation_error(f"Required query parameter missing: {param_name}")
|
||||||
|
|
||||||
|
if not param_exists:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if param_exists and param_type != SegmentType.STRING:
|
||||||
|
param_value = query_params[param_name]
|
||||||
|
validation_result = cls._validate_form_parameter_type(param_name, param_value, param_type)
|
||||||
|
if not validation_result["valid"]:
|
||||||
|
return validation_result
|
||||||
|
|
||||||
return {"valid": True}
|
return {"valid": True}
|
||||||
|
|
||||||
|
|||||||
@ -26,12 +26,30 @@ class TestWebhookServiceUnit:
|
|||||||
|
|
||||||
assert webhook_data["method"] == "POST"
|
assert webhook_data["method"] == "POST"
|
||||||
assert webhook_data["headers"]["Authorization"] == "Bearer token"
|
assert webhook_data["headers"]["Authorization"] == "Bearer token"
|
||||||
assert webhook_data["query_params"]["version"] == "1"
|
assert webhook_data["query_params"]["version"] == 1
|
||||||
assert webhook_data["query_params"]["format"] == "json"
|
assert webhook_data["query_params"]["format"] == "json"
|
||||||
assert webhook_data["body"]["message"] == "hello"
|
assert webhook_data["body"]["message"] == "hello"
|
||||||
assert webhook_data["body"]["count"] == 42
|
assert webhook_data["body"]["count"] == 42
|
||||||
assert webhook_data["files"] == {}
|
assert webhook_data["files"] == {}
|
||||||
|
|
||||||
|
def test_extract_webhook_data_query_params_remain_strings(self):
|
||||||
|
"""Query parameters remain raw strings during extraction."""
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
with app.test_request_context(
|
||||||
|
"/webhook",
|
||||||
|
method="GET",
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
query_string="count=42&threshold=3.14&enabled=true¬e=text",
|
||||||
|
):
|
||||||
|
webhook_trigger = MagicMock()
|
||||||
|
webhook_data = WebhookService.extract_webhook_data(webhook_trigger)
|
||||||
|
|
||||||
|
assert webhook_data["query_params"]["count"] == 42
|
||||||
|
assert webhook_data["query_params"]["threshold"] == 3.14
|
||||||
|
assert webhook_data["query_params"]["enabled"] == True
|
||||||
|
assert webhook_data["query_params"]["note"] == "text"
|
||||||
|
|
||||||
def test_extract_webhook_data_form_urlencoded(self):
|
def test_extract_webhook_data_form_urlencoded(self):
|
||||||
"""Test webhook data extraction from form URL encoded request."""
|
"""Test webhook data extraction from form URL encoded request."""
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -182,6 +200,92 @@ class TestWebhookServiceUnit:
|
|||||||
assert result["valid"] is False
|
assert result["valid"] is False
|
||||||
assert "Required query parameter missing: version" in result["error"]
|
assert "Required query parameter missing: version" in result["error"]
|
||||||
|
|
||||||
|
def test_validate_webhook_request_query_param_number_type(self):
|
||||||
|
"""Numeric query parameters should validate with numeric types."""
|
||||||
|
webhook_data = {
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {},
|
||||||
|
"query_params": {"count": "42"},
|
||||||
|
"body": {},
|
||||||
|
"files": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
node_config = {
|
||||||
|
"data": {
|
||||||
|
"method": "post",
|
||||||
|
"params": [{"name": "count", "required": True, "type": "number"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||||
|
|
||||||
|
assert result["valid"] is True
|
||||||
|
|
||||||
|
def test_validate_webhook_request_query_param_number_type_invalid(self):
|
||||||
|
"""Numeric query parameter validation should fail for non-numeric values."""
|
||||||
|
webhook_data = {
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {},
|
||||||
|
"query_params": {"count": "forty-two"},
|
||||||
|
"body": {},
|
||||||
|
"files": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
node_config = {
|
||||||
|
"data": {
|
||||||
|
"method": "post",
|
||||||
|
"params": [{"name": "count", "required": True, "type": "number"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||||
|
|
||||||
|
assert result["valid"] is False
|
||||||
|
assert "must be a valid number" in result["error"]
|
||||||
|
|
||||||
|
def test_validate_webhook_request_query_param_boolean_type(self):
|
||||||
|
"""Boolean query parameters should validate with supported boolean strings."""
|
||||||
|
webhook_data = {
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {},
|
||||||
|
"query_params": {"enabled": "true"},
|
||||||
|
"body": {},
|
||||||
|
"files": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
node_config = {
|
||||||
|
"data": {
|
||||||
|
"method": "post",
|
||||||
|
"params": [{"name": "enabled", "required": True, "type": "boolean"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||||
|
|
||||||
|
assert result["valid"] is True
|
||||||
|
|
||||||
|
def test_validate_webhook_request_query_param_string_type_preserved(self):
|
||||||
|
"""String typed query parameters remain as strings even if boolean-like."""
|
||||||
|
webhook_data = {
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {},
|
||||||
|
"query_params": {"flag": "true"},
|
||||||
|
"body": {},
|
||||||
|
"files": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
node_config = {
|
||||||
|
"data": {
|
||||||
|
"method": "post",
|
||||||
|
"params": [{"name": "flag", "required": True, "type": "string"}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WebhookService.validate_webhook_request(webhook_data, node_config)
|
||||||
|
|
||||||
|
assert result["valid"] is True
|
||||||
|
assert webhook_data["query_params"]["flag"] == "true"
|
||||||
|
|
||||||
def test_validate_webhook_request_missing_required_body_param(self):
|
def test_validate_webhook_request_missing_required_body_param(self):
|
||||||
"""Test webhook validation with missing required body parameter."""
|
"""Test webhook validation with missing required body parameter."""
|
||||||
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
webhook_data = {"method": "POST", "headers": {}, "query_params": {}, "body": {}, "files": {}}
|
||||||
@ -515,7 +619,7 @@ class TestWebhookServiceUnit:
|
|||||||
assert "must be an array of numbers" in result["error"]
|
assert "must be an array of numbers" in result["error"]
|
||||||
|
|
||||||
def test_validate_json_parameter_type_array_bool(self):
|
def test_validate_json_parameter_type_array_bool(self):
|
||||||
"""Test JSON parameter type validation for array[bool] type."""
|
"""Test JSON parameter type validation for array[boolean] type."""
|
||||||
# Valid array of booleans
|
# Valid array of booleans
|
||||||
result = WebhookService._validate_json_parameter_type("flags", [True, False, True], "array[boolean]")
|
result = WebhookService._validate_json_parameter_type("flags", [True, False, True], "array[boolean]")
|
||||||
assert result["valid"] is True
|
assert result["valid"] is True
|
||||||
@ -540,12 +644,6 @@ class TestWebhookServiceUnit:
|
|||||||
assert result["valid"] is False
|
assert result["valid"] is False
|
||||||
assert "must be an array of objects" in result["error"]
|
assert "must be an array of objects" in result["error"]
|
||||||
|
|
||||||
def test_validate_json_parameter_type_unknown_type(self):
|
|
||||||
"""Test JSON parameter type validation for unknown type."""
|
|
||||||
# Unknown type should return valid and log warning
|
|
||||||
result = WebhookService._validate_json_parameter_type("data", "anything", "unknown_type")
|
|
||||||
assert result["valid"] is True
|
|
||||||
|
|
||||||
def test_validate_webhook_request_json_type_validation(self):
|
def test_validate_webhook_request_json_type_validation(self):
|
||||||
"""Test webhook validation with JSON parameter type validation."""
|
"""Test webhook validation with JSON parameter type validation."""
|
||||||
# Test valid JSON types
|
# Test valid JSON types
|
||||||
@ -573,11 +671,11 @@ class TestWebhookServiceUnit:
|
|||||||
"body": [
|
"body": [
|
||||||
{"name": "name", "type": "string", "required": True},
|
{"name": "name", "type": "string", "required": True},
|
||||||
{"name": "age", "type": "number", "required": True},
|
{"name": "age", "type": "number", "required": True},
|
||||||
{"name": "active", "type": "bool", "required": True},
|
{"name": "active", "type": "boolean", "required": True},
|
||||||
{"name": "profile", "type": "object", "required": True},
|
{"name": "profile", "type": "object", "required": True},
|
||||||
{"name": "tags", "type": "array[string]", "required": True},
|
{"name": "tags", "type": "array[string]", "required": True},
|
||||||
{"name": "scores", "type": "array[number]", "required": True},
|
{"name": "scores", "type": "array[number]", "required": True},
|
||||||
{"name": "flags", "type": "array[bool]", "required": True},
|
{"name": "flags", "type": "array[boolean]", "required": True},
|
||||||
{"name": "items", "type": "array[object]", "required": True},
|
{"name": "items", "type": "array[object]", "required": True},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user