diff --git a/api/controllers/trigger/webhook.py b/api/controllers/trigger/webhook.py index cec5c3d8ae..22b24271c6 100644 --- a/api/controllers/trigger/webhook.py +++ b/api/controllers/trigger/webhook.py @@ -1,7 +1,7 @@ import logging import time -from flask import jsonify +from flask import jsonify, request from werkzeug.exceptions import NotFound, RequestEntityTooLarge from controllers.trigger import bp @@ -28,8 +28,14 @@ def _prepare_webhook_execution(webhook_id: str, is_debug: bool = False): webhook_data = WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config) return webhook_trigger, workflow, node_config, webhook_data, None except ValueError as e: - # Fall back to raw extraction for error reporting - webhook_data = WebhookService.extract_webhook_data(webhook_trigger) + # Provide minimal context for error reporting without risking another parse failure + webhook_data = { + "method": request.method, + "headers": dict(request.headers), + "query_params": dict(request.args), + "body": {}, + "files": {}, + } return webhook_trigger, workflow, node_config, webhook_data, str(e) diff --git a/api/services/trigger/webhook_service.py b/api/services/trigger/webhook_service.py index 6e0ee7a191..4b3e1330fd 100644 --- a/api/services/trigger/webhook_service.py +++ b/api/services/trigger/webhook_service.py @@ -5,6 +5,7 @@ import secrets from collections.abc import Mapping from typing import Any +import orjson from flask import request from pydantic import BaseModel from sqlalchemy import select @@ -169,7 +170,7 @@ class WebhookService: - method: HTTP method - headers: Request headers - query_params: Query parameters as strings - - body: Request body (varies by content type) + - body: Request body (varies by content type; JSON parsing errors raise ValueError) - files: Uploaded files (if any) """ cls._validate_content_length() @@ -255,14 +256,21 @@ class WebhookService: Returns: tuple: (body_data, files_data) where: - - body_data: Parsed JSON content or empty dict if parsing fails + - body_data: Parsed JSON content - files_data: Empty dict (JSON requests don't contain files) + + Raises: + ValueError: If JSON parsing fails """ + raw_body = request.get_data(cache=True) + if not raw_body or raw_body.strip() == b"": + return {}, {} + try: - body = request.get_json() or {} - except Exception: - logger.warning("Failed to parse JSON body") - body = {} + body = orjson.loads(raw_body) + except orjson.JSONDecodeError as exc: + logger.warning("Failed to parse JSON body: %s", exc) + raise ValueError(f"Invalid JSON body: {exc}") from exc return body, {} @classmethod diff --git a/api/tests/unit_tests/services/test_webhook_service.py b/api/tests/unit_tests/services/test_webhook_service.py index 010295bcd6..6afe52d97b 100644 --- a/api/tests/unit_tests/services/test_webhook_service.py +++ b/api/tests/unit_tests/services/test_webhook_service.py @@ -118,10 +118,8 @@ class TestWebhookServiceUnit: "/webhook", method="POST", headers={"Content-Type": "application/json"}, data="invalid json" ): webhook_trigger = MagicMock() - webhook_data = WebhookService.extract_webhook_data(webhook_trigger) - - assert webhook_data["method"] == "POST" - assert webhook_data["body"] == {} # Should default to empty dict + with pytest.raises(ValueError, match="Invalid JSON body"): + WebhookService.extract_webhook_data(webhook_trigger) def test_generate_webhook_response_default(self): """Test webhook response generation with default values.""" @@ -435,6 +433,27 @@ class TestWebhookServiceUnit: assert result["body"]["message"] == "hello" # Already string assert result["body"]["age"] == 25 # Already number + def test_extract_and_validate_webhook_data_invalid_json_error(self): + """Invalid JSON should bubble up as a ValueError with details.""" + app = Flask(__name__) + + with app.test_request_context( + "/webhook", + method="POST", + headers={"Content-Type": "application/json"}, + data='{"invalid": }', + ): + webhook_trigger = MagicMock() + node_config = { + "data": { + "method": "post", + "content_type": "application/json", + } + } + + with pytest.raises(ValueError, match="Invalid JSON body"): + WebhookService.extract_and_validate_webhook_data(webhook_trigger, node_config) + def test_extract_and_validate_webhook_data_validation_error(self): """Test unified data extraction with validation error.""" app = Flask(__name__)