Files
dify/api/controllers/trigger/webhook.py
T
CodingOnStar 954ba55eb3 Merge remote-tracking branch 'origin/main' into song/snp-682-ip-allowlist
# Conflicts:
#	api/controllers/mcp/mcp.py
#	api/controllers/openapi/__init__.py
#	api/controllers/web/login.py
#	api/controllers/web/wraps.py
#	api/extensions/ext_application_services.py
#	api/openapi/markdown/console-openapi.md
#	api/services/webapp_access_query_service.py
#	api/tests/unit_tests/controllers/web/test_web_login.py
#	api/tests/unit_tests/controllers/web/test_wraps.py
#	api/tests/unit_tests/extensions/test_ext_application_services.py
#	packages/contracts/generated/api/console/apps/orpc.gen.ts
#	packages/contracts/generated/api/console/apps/types.gen.ts
#	web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx
#	web/app/components/app/access-point/index.tsx
#	web/app/components/base/chat/chat/hooks.ts
#	web/app/components/base/file-uploader/hooks.ts
#	web/app/components/base/image-uploader/__tests__/hooks.spec.ts
#	web/app/components/base/image-uploader/hooks.ts
#	web/app/components/explore/try-app/index.tsx
#	web/features/agent-v2/agent-detail/__tests__/layout.spec.tsx
#	web/i18n/ar-TN/common.json
#	web/i18n/de-DE/common.json
#	web/i18n/en-US/common.json
#	web/i18n/es-ES/common.json
#	web/i18n/fa-IR/common.json
#	web/i18n/fr-FR/common.json
#	web/i18n/hi-IN/common.json
#	web/i18n/id-ID/common.json
#	web/i18n/it-IT/common.json
#	web/i18n/ja-JP/common.json
#	web/i18n/ko-KR/common.json
#	web/i18n/lo-LA/common.json
#	web/i18n/nl-NL/common.json
#	web/i18n/pl-PL/common.json
#	web/i18n/pt-BR/common.json
#	web/i18n/ro-RO/common.json
#	web/i18n/ru-RU/common.json
#	web/i18n/sl-SI/common.json
#	web/i18n/th-TH/common.json
#	web/i18n/tr-TR/common.json
#	web/i18n/uk-UA/common.json
#	web/i18n/vi-VN/common.json
#	web/i18n/zh-Hans/common.json
#	web/i18n/zh-Hant/common.json
#	web/service/console/query-policies.ts
2026-09-29 16:53:53 +08:00

171 lines
6.8 KiB
Python

import logging
import time
from flask import jsonify, request
from werkzeug.exceptions import RequestEntityTooLarge
from controllers.common.access_response import webhook_not_found_response
from controllers.trigger import bp
from core.trigger.debug.event_bus import TriggerDebugEventBus
from core.trigger.debug.events import WebhookDebugEvent, build_webhook_pool_key
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
logger = logging.getLogger(__name__)
_QUOTA_EXCEEDED_MESSAGES = {
QuotaType.TRIGGER: "Trigger event quota exceeded. Please upgrade your plan.",
QuotaType.WORKFLOW: "Workflow execution quota exceeded. Please upgrade your plan.",
}
_DEFAULT_QUOTA_EXCEEDED_MESSAGE = "Quota exceeded. Please upgrade your plan."
def _get_quota_exceeded_message(feature: str) -> str:
try:
quota_type = QuotaType(feature)
except ValueError:
return _DEFAULT_QUOTA_EXCEEDED_MESSAGE
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
)
webhook_data: RawWebhookDataDict
try:
# Use new unified extraction and validation
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:
# 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)
@bp.route("/webhook/<string:webhook_id>", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
def handle_webhook(webhook_id: str):
"""
Handle webhook trigger calls.
This endpoint receives webhook calls and processes them according to the
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
# Process webhook call (send to Celery)
WebhookService.trigger_workflow_execution(
webhook_trigger,
webhook_data,
workflow,
end_users=application_services().app_scoped_end_users.commands,
)
# Return configured response
response_data, status_code = WebhookService.generate_webhook_response(node_config)
return jsonify(response_data), status_code
except QuotaExceededError as error:
return jsonify(
{
"error": "Too Many Requests",
"message": _get_quota_exceeded_message(error.feature),
}
), 429
except ValueError:
return webhook_not_found_response()
except RequestEntityTooLarge:
raise
except Exception as e:
logger.exception("Webhook processing failed for %s", webhook_id)
return jsonify({"error": "Internal server error", "message": str(e)}), 500
@bp.route("/webhook-debug/<string:webhook_id>", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
def handle_webhook_debug(webhook_id: str):
"""Handle webhook debug calls without triggering production workflow execution.
The debug webhook endpoint is only for draft inspection flows. It never enqueues
Celery work for the published workflow; instead it dispatches an in-memory debug
event to an active Variable Inspector listener. Returning a clear error when no
listener is registered prevents a misleading 200 response for requests that are
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
workflow_inputs = WebhookService.build_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,
)
event = WebhookDebugEvent(
request_id=f"webhook_debug_{webhook_trigger.webhook_id}_{int(time.time() * 1000)}",
timestamp=int(time.time()),
node_id=webhook_trigger.node_id,
payload={
"inputs": workflow_inputs,
"webhook_data": webhook_data,
"method": webhook_data.get("method"),
},
)
dispatch_count = TriggerDebugEventBus.dispatch(
tenant_id=webhook_trigger.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,
)
return (
jsonify(
{
"error": "No active debug listener",
"message": (
"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,
}
),
409,
)
response_data, status_code = WebhookService.generate_webhook_response(node_config)
return jsonify(response_data), status_code
except ValueError:
return webhook_not_found_response()
except RequestEntityTooLarge:
raise
except Exception as e:
logger.exception("Webhook debug processing failed for %s", webhook_id)
return jsonify({"error": "Internal server error", "message": "An internal error has occurred."}), 500