mirror of
https://github.com/langgenius/dify.git
synced 2026-11-13 14:51:39 +08:00
refactor(api): isolate native webhook runtime (#45912)
Co-authored-by: Stream <Stream_2@qq.com>
This commit is contained in:
1 parent
af37c00fb3
commit
9fbd9dcd33
22 files changed
+2500
-3931
No files matched your search
@@ -1,17 +1,21 @@
|
||||
import logging
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
|
||||
from flask import jsonify, request
|
||||
from werkzeug.exceptions import RequestEntityTooLarge
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from controllers.common.access_response import webhook_not_found_response
|
||||
from controllers.trigger import bp
|
||||
from controllers.trigger.webhook_request import WebhookRequestParser, WebhookRequestValidationError
|
||||
from core.trigger.debug.event_bus import TriggerDebugEventBus
|
||||
from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key
|
||||
from core.trigger.utils.endpoint import generate_webhook_trigger_endpoint
|
||||
from enums import QuotaType
|
||||
from extensions.ext_application_services import application_services
|
||||
from services.errors.app import QuotaExceededError
|
||||
from services.trigger.webhook_service import RawWebhookDataDict, WebhookService
|
||||
from services.trigger.webhook_service import WebhookContext, WebhookNotFoundError, build_webhook_workflow_inputs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,40 +34,19 @@ def _get_quota_exceeded_message(feature: str) -> str:
|
||||
return _QUOTA_EXCEEDED_MESSAGES.get(quota_type, _DEFAULT_QUOTA_EXCEEDED_MESSAGE)
|
||||
|
||||
|
||||
def _prepare_webhook_execution(webhook_id: str, is_debug: bool = False):
|
||||
"""Fetch trigger context, extract request data, and validate payload using unified processing.
|
||||
|
||||
Args:
|
||||
webhook_id: The webhook ID to process
|
||||
is_debug: If True, skip status validation for debug mode
|
||||
"""
|
||||
webhook_trigger, workflow, node_config = WebhookService.get_webhook_trigger_and_workflow(
|
||||
webhook_id, is_debug=is_debug
|
||||
def _prepare_webhook_execution(webhook_id: str, *, debug: bool) -> tuple[WebhookContext, WebhookRequestParser]:
|
||||
services = application_services().workspaces
|
||||
context = services.webhooks.prepare(webhook_id=webhook_id, debug=debug)
|
||||
parser = WebhookRequestParser(
|
||||
request=request,
|
||||
files=services.webhook_files,
|
||||
resolve_actor=lambda webhook_trigger: services.webhooks.resolve_actor(context=webhook_trigger),
|
||||
)
|
||||
|
||||
webhook_data: RawWebhookDataDict
|
||||
try:
|
||||
# Use new unified extraction and validation
|
||||
webhook_data = WebhookService.extract_and_validate_webhook_data(
|
||||
webhook_trigger,
|
||||
node_config,
|
||||
end_users=application_services().app_scoped_end_users.commands,
|
||||
)
|
||||
return webhook_trigger, workflow, node_config, webhook_data, None
|
||||
except ValueError as e:
|
||||
# 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)
|
||||
return context, parser
|
||||
|
||||
|
||||
@bp.route("/webhook/<string:webhook_id>", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
|
||||
def handle_webhook(webhook_id: str):
|
||||
def handle_webhook(webhook_id: str) -> Response | tuple[Response, int]:
|
||||
"""
|
||||
Handle webhook trigger calls.
|
||||
|
||||
@@ -71,22 +54,17 @@ def handle_webhook(webhook_id: str):
|
||||
configured webhook trigger settings.
|
||||
"""
|
||||
try:
|
||||
webhook_trigger, workflow, node_config, webhook_data, error = _prepare_webhook_execution(webhook_id)
|
||||
if error:
|
||||
return jsonify({"error": "Bad Request", "message": error}), 400
|
||||
context, parser = _prepare_webhook_execution(webhook_id, debug=False)
|
||||
try:
|
||||
webhook_data = parser.extract_and_validate_webhook_data(context, context.node_config)
|
||||
except WebhookRequestValidationError as error:
|
||||
return jsonify({"error": "Bad Request", "message": error.public_message}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
# Process webhook call (send to Celery)
|
||||
WebhookService.trigger_workflow_execution(
|
||||
webhook_trigger,
|
||||
webhook_data,
|
||||
workflow,
|
||||
end_users=application_services().app_scoped_end_users.commands,
|
||||
trigger_status=application_services().apps.trigger_status,
|
||||
workflow_dispatch=application_services().workspaces.workflow_dispatch,
|
||||
)
|
||||
application_services().workspaces.webhooks.execute(context=context, data=webhook_data, actor=parser.actor)
|
||||
|
||||
# Return configured response
|
||||
response_data, status_code = WebhookService.generate_webhook_response(node_config)
|
||||
response_data, status_code = parser.generate_webhook_response(context.node_config)
|
||||
return jsonify(response_data), status_code
|
||||
|
||||
except QuotaExceededError as error:
|
||||
@@ -95,18 +73,20 @@ def handle_webhook(webhook_id: str):
|
||||
"error": "Too Many Requests",
|
||||
"message": _get_quota_exceeded_message(error.feature),
|
||||
}
|
||||
), 429
|
||||
except ValueError:
|
||||
), HTTPStatus.TOO_MANY_REQUESTS
|
||||
except WebhookNotFoundError:
|
||||
return webhook_not_found_response()
|
||||
except RequestEntityTooLarge:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Webhook processing failed for %s", webhook_id)
|
||||
return jsonify({"error": "Internal server error", "message": "An internal error has occurred."}), 500
|
||||
return jsonify(
|
||||
{"error": "Internal server error", "message": "An internal error has occurred."}
|
||||
), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
|
||||
|
||||
@bp.route("/webhook-debug/<string:webhook_id>", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
|
||||
def handle_webhook_debug(webhook_id: str):
|
||||
def handle_webhook_debug(webhook_id: str) -> Response | tuple[Response, int]:
|
||||
"""Handle webhook debug calls without triggering production workflow execution.
|
||||
|
||||
The debug webhook endpoint is only for draft inspection flows. It never enqueues
|
||||
@@ -116,22 +96,24 @@ def handle_webhook_debug(webhook_id: str):
|
||||
effectively dropped.
|
||||
"""
|
||||
try:
|
||||
webhook_trigger, _, node_config, webhook_data, error = _prepare_webhook_execution(webhook_id, is_debug=True)
|
||||
if error:
|
||||
return jsonify({"error": "Bad Request", "message": error}), 400
|
||||
context, parser = _prepare_webhook_execution(webhook_id, debug=True)
|
||||
try:
|
||||
webhook_data = parser.extract_and_validate_webhook_data(context, context.node_config)
|
||||
except WebhookRequestValidationError as error:
|
||||
return jsonify({"error": "Bad Request", "message": error.public_message}), HTTPStatus.BAD_REQUEST
|
||||
|
||||
workflow_inputs = WebhookService.build_workflow_inputs(webhook_data)
|
||||
workflow_inputs = build_webhook_workflow_inputs(webhook_data)
|
||||
|
||||
# Generate pool key and dispatch debug event
|
||||
pool_key: str = build_webhook_pool_key(
|
||||
tenant_id=webhook_trigger.tenant_id,
|
||||
app_id=webhook_trigger.app_id,
|
||||
node_id=webhook_trigger.node_id,
|
||||
tenant_id=context.tenant_id,
|
||||
app_id=context.app_id,
|
||||
node_id=context.node_id,
|
||||
)
|
||||
event = WebhookDebugEvent(
|
||||
request_id=f"webhook_debug_{webhook_trigger.webhook_id}_{int(time.time() * 1000)}",
|
||||
request_id=f"webhook_debug_{context.webhook_id}_{int(time.time() * 1000)}",
|
||||
timestamp=int(time.time()),
|
||||
node_id=webhook_trigger.node_id,
|
||||
node_id=context.node_id,
|
||||
payload={
|
||||
"inputs": workflow_inputs,
|
||||
"webhook_data": webhook_data,
|
||||
@@ -139,17 +121,17 @@ def handle_webhook_debug(webhook_id: str):
|
||||
},
|
||||
)
|
||||
dispatch_count = TriggerDebugEventBus.dispatch(
|
||||
tenant_id=webhook_trigger.tenant_id,
|
||||
tenant_id=context.tenant_id,
|
||||
event=event,
|
||||
pool_key=pool_key,
|
||||
)
|
||||
if dispatch_count == 0:
|
||||
logger.warning(
|
||||
"Webhook debug request dropped without an active listener for webhook %s (tenant=%s, app=%s, node=%s)",
|
||||
webhook_trigger.webhook_id,
|
||||
webhook_trigger.tenant_id,
|
||||
webhook_trigger.app_id,
|
||||
webhook_trigger.node_id,
|
||||
context.webhook_id,
|
||||
context.tenant_id,
|
||||
context.app_id,
|
||||
context.node_id,
|
||||
)
|
||||
return (
|
||||
jsonify(
|
||||
@@ -159,18 +141,20 @@ def handle_webhook_debug(webhook_id: str):
|
||||
"The webhook debug URL only works while the Variable Inspector is listening. "
|
||||
"Use the published webhook URL to execute the workflow in Celery."
|
||||
),
|
||||
"execution_url": webhook_trigger.webhook_url,
|
||||
"execution_url": generate_webhook_trigger_endpoint(context.webhook_id),
|
||||
}
|
||||
),
|
||||
409,
|
||||
HTTPStatus.CONFLICT,
|
||||
)
|
||||
response_data, status_code = WebhookService.generate_webhook_response(node_config)
|
||||
response_data, status_code = parser.generate_webhook_response(context.node_config)
|
||||
return jsonify(response_data), status_code
|
||||
|
||||
except ValueError:
|
||||
except WebhookNotFoundError:
|
||||
return webhook_not_found_response()
|
||||
except RequestEntityTooLarge:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Webhook debug processing failed for %s", webhook_id)
|
||||
return jsonify({"error": "Internal server error", "message": "An internal error has occurred."}), 500
|
||||
return jsonify(
|
||||
{"error": "Internal server error", "message": "An internal error has occurred."}
|
||||
), HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
Reference in new issue
Block a user