mirror of
https://github.com/langgenius/dify.git
synced 2026-07-23 20:18:40 +08:00
chore: improve lambda plugin runtime error display (#39437)
This commit is contained in:
parent
1835e8ef31
commit
a3c18c561e
@ -1,7 +1,7 @@
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable, Generator
|
||||
from collections.abc import Callable, Generator, Mapping
|
||||
from typing import Any, cast
|
||||
from urllib.parse import unquote
|
||||
|
||||
@ -23,6 +23,7 @@ from core.plugin.impl.exc import (
|
||||
PluginLLMPollingUnsupportedError,
|
||||
PluginNotFoundError,
|
||||
PluginPermissionDeniedError,
|
||||
PluginRuntimeError,
|
||||
PluginUniqueIdentifierError,
|
||||
)
|
||||
from core.trigger.errors import (
|
||||
@ -375,6 +376,18 @@ class BasePluginClient:
|
||||
# type `PluginLLMPollingUnsupportedError`.
|
||||
case PluginLLMPollingUnsupportedError.__name__:
|
||||
raise PluginLLMPollingUnsupportedError(description=error_object.get("message"))
|
||||
case PluginRuntimeError.__name__:
|
||||
args = error_object.get("args")
|
||||
lambda_request_id = args.get("request_id") if isinstance(args, Mapping) else None
|
||||
if not isinstance(lambda_request_id, str):
|
||||
lambda_request_id = None
|
||||
runtime_message = error_object.get("message")
|
||||
if not isinstance(runtime_message, str):
|
||||
runtime_message = "Plugin runtime request failed"
|
||||
raise PluginRuntimeError(
|
||||
description=runtime_message,
|
||||
lambda_request_id=lambda_request_id,
|
||||
)
|
||||
case _:
|
||||
raise PluginInvokeError(description=message)
|
||||
case PluginDaemonInternalServerError.__name__:
|
||||
|
||||
@ -49,6 +49,18 @@ class PluginDaemonBadRequestError(PluginDaemonClientSideError):
|
||||
description: str = "Bad Request"
|
||||
|
||||
|
||||
class PluginRuntimeError(PluginDaemonInternalError):
|
||||
"""A plugin runtime failed before it could return a valid plugin response."""
|
||||
|
||||
lambda_request_id: str | None
|
||||
|
||||
def __init__(self, description: str, lambda_request_id: str | None = None) -> None:
|
||||
self.lambda_request_id = lambda_request_id
|
||||
if lambda_request_id:
|
||||
description = description.replace(f"RequestId: {lambda_request_id} Error: ", "", 1)
|
||||
super().__init__(description)
|
||||
|
||||
|
||||
class PluginInvokeError(PluginDaemonClientSideError, ValueError):
|
||||
description: str = "Invoke Error"
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ from werkzeug.http import HTTP_STATUS_CODES
|
||||
|
||||
from configs import dify_config
|
||||
from core.errors.error import AppInvokeQuotaExceededError
|
||||
from core.plugin.impl.exc import PluginRuntimeError
|
||||
from extensions.ext_logging import get_request_id
|
||||
from libs.flask_restx_compat import install_swagger_compatibility
|
||||
from libs.token import build_force_logout_cookie_headers
|
||||
|
||||
@ -100,6 +102,20 @@ def register_external_error_handlers(api: Api, body_formatter: ErrorBodyFormatte
|
||||
data = {"code": "too_many_requests", "message": str(e), "status": status_code}
|
||||
return _finalize(e, data, status_code), status_code
|
||||
|
||||
def handle_plugin_runtime_error(e: PluginRuntimeError):
|
||||
got_request_exception.send(current_app, exception=e)
|
||||
status_code = 502
|
||||
details = {"request_id": get_request_id()}
|
||||
if e.lambda_request_id:
|
||||
details["lambda_request_id"] = e.lambda_request_id
|
||||
data = {
|
||||
"code": "plugin_runtime_error",
|
||||
"message": e.description,
|
||||
"details": details,
|
||||
"status": status_code,
|
||||
}
|
||||
return _finalize(e, data, status_code), status_code
|
||||
|
||||
def handle_general_exception(e: Exception):
|
||||
got_request_exception.send(current_app, exception=e)
|
||||
|
||||
@ -121,6 +137,7 @@ def register_external_error_handlers(api: Api, body_formatter: ErrorBodyFormatte
|
||||
api.errorhandler(HTTPException)(handle_http_exception)
|
||||
api.errorhandler(ValueError)(handle_value_error)
|
||||
api.errorhandler(AppInvokeQuotaExceededError)(handle_quota_exceeded)
|
||||
api.errorhandler(PluginRuntimeError)(handle_plugin_runtime_error)
|
||||
api.errorhandler(Exception)(handle_general_exception)
|
||||
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ from pytest_mock import MockerFixture
|
||||
from core.plugin.endpoint.exc import EndpointSetupFailedError
|
||||
from core.plugin.entities.plugin_daemon import PluginDaemonInnerError
|
||||
from core.plugin.impl.base import PLUGIN_DAEMON_MAX_PATH_LENGTH, BasePluginClient
|
||||
from core.plugin.impl.exc import PluginLLMPollingUnsupportedError
|
||||
from core.plugin.impl.exc import PluginLLMPollingUnsupportedError, PluginRuntimeError
|
||||
from core.trigger.errors import (
|
||||
EventIgnoreError,
|
||||
TriggerInvokeError,
|
||||
@ -175,3 +175,25 @@ class TestBasePluginClientImpl:
|
||||
|
||||
with pytest.raises(PluginLLMPollingUnsupportedError):
|
||||
client._handle_plugin_daemon_error("PluginInvokeError", message)
|
||||
|
||||
def test_handle_plugin_daemon_error_maps_runtime_error_to_typed_exception(self):
|
||||
client = BasePluginClient()
|
||||
lambda_request_id = "45664803-3d3c-4d4f-93fe-e3b19e43092b"
|
||||
message = json.dumps(
|
||||
{
|
||||
"error_type": PluginRuntimeError.__name__,
|
||||
"message": (
|
||||
"Plugin runtime request failed: Runtime.ExitError: "
|
||||
f"RequestId: {lambda_request_id} Error: Runtime exited with error: exit status 1"
|
||||
),
|
||||
"args": {"request_id": lambda_request_id, "status_code": 200},
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.raises(PluginRuntimeError) as exc_info:
|
||||
client._handle_plugin_daemon_error("PluginInvokeError", message)
|
||||
|
||||
assert exc_info.value.description == (
|
||||
"Plugin runtime request failed: Runtime.ExitError: Runtime exited with error: exit status 1"
|
||||
)
|
||||
assert exc_info.value.lambda_request_id == lambda_request_id
|
||||
|
||||
@ -4,6 +4,7 @@ from werkzeug.exceptions import BadRequest, Unauthorized
|
||||
|
||||
from constants import COOKIE_NAME_ACCESS_TOKEN, COOKIE_NAME_CSRF_TOKEN, COOKIE_NAME_REFRESH_TOKEN
|
||||
from core.errors.error import AppInvokeQuotaExceededError
|
||||
from core.plugin.impl.exc import PluginRuntimeError
|
||||
from libs.exception import BaseHTTPException
|
||||
from libs.external_api import ExternalApi
|
||||
from libs.rate_limit import _BearerRateLimited
|
||||
@ -39,6 +40,14 @@ def _create_api_app():
|
||||
def get(self):
|
||||
raise RuntimeError("oops")
|
||||
|
||||
@api.route("/plugin-runtime-error")
|
||||
class PluginRuntime(Resource):
|
||||
def get(self):
|
||||
raise PluginRuntimeError(
|
||||
"Plugin runtime request failed: Runtime.ExitError: Runtime exited with error: exit status 1",
|
||||
lambda_request_id="lambda-request-id",
|
||||
)
|
||||
|
||||
# Note: We avoid altering default_mediatype to keep normal error paths
|
||||
|
||||
# Special 400 message rewrite
|
||||
@ -107,6 +116,24 @@ def test_external_api_json_message_and_bad_request_rewrite():
|
||||
assert res.get_json()["message"] == "Invalid JSON payload received or JSON payload is empty."
|
||||
|
||||
|
||||
def test_external_api_plugin_runtime_error(mocker):
|
||||
mocker.patch("libs.external_api.get_request_id", return_value="api-request-id")
|
||||
app = _create_api_app()
|
||||
|
||||
res = app.test_client().get("/api/plugin-runtime-error")
|
||||
|
||||
assert res.status_code == 502
|
||||
assert res.get_json() == {
|
||||
"code": "plugin_runtime_error",
|
||||
"message": "Plugin runtime request failed: Runtime.ExitError: Runtime exited with error: exit status 1",
|
||||
"details": {
|
||||
"request_id": "api-request-id",
|
||||
"lambda_request_id": "lambda-request-id",
|
||||
},
|
||||
"status": 502,
|
||||
}
|
||||
|
||||
|
||||
def test_external_api_param_mapping_and_quota():
|
||||
app = _create_api_app()
|
||||
client = app.test_client()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user