diff --git a/api/controllers/console/billing/billing.py b/api/controllers/console/billing/billing.py index da162ac49a8..27e3049f647 100644 --- a/api/controllers/console/billing/billing.py +++ b/api/controllers/console/billing/billing.py @@ -96,8 +96,8 @@ class PartnerTenants(Resource): try: click_id = req_data.click_id decoded_partner_key = base64.b64decode(partner_key).decode("utf-8") - except Exception: - raise BadRequest("Invalid partner_key") + except Exception as e: + raise BadRequest("Invalid partner_key") from e if not click_id or not decoded_partner_key or not current_user.id: raise BadRequest("Invalid partner information") diff --git a/api/controllers/console/workspace/workspace.py b/api/controllers/console/workspace/workspace.py index 4ded2d8fd85..38c21bc8d44 100644 --- a/api/controllers/console/workspace/workspace.py +++ b/api/controllers/console/workspace/workspace.py @@ -302,8 +302,8 @@ class SwitchWorkspaceApi(Resource): # Check whether the tenant_id belongs to the current account. try: TenantService.switch_tenant(current_user, args.tenant_id, session=session) - except Exception: - raise AccountNotLinkTenantError("Account not link tenant") + except Exception as e: + raise AccountNotLinkTenantError("Account not link tenant") from e new_tenant = TenantService.get_tenant_by_id(args.tenant_id, session=session) if new_tenant is None: diff --git a/api/controllers/files/tool_files.py b/api/controllers/files/tool_files.py index ef47485d800..03f635627de 100644 --- a/api/controllers/files/tool_files.py +++ b/api/controllers/files/tool_files.py @@ -67,8 +67,8 @@ class ToolFileApi(Resource): except NotFound: raise - except Exception: - raise UnsupportedFileTypeError() + except Exception as e: + raise UnsupportedFileTypeError() from e mime_type = tool_file.mime_type filename = tool_file.filename diff --git a/api/controllers/inner_api/plugin/wraps.py b/api/controllers/inner_api/plugin/wraps.py index cc68fab6c99..3b58d82bdb8 100644 --- a/api/controllers/inner_api/plugin/wraps.py +++ b/api/controllers/inner_api/plugin/wraps.py @@ -84,8 +84,8 @@ def get_user(tenant_id: str, user_id: str | None) -> EndUser: session.flush() session.refresh(user_model) - except Exception: - raise ValueError("user not found") + except Exception as e: + raise ValueError("user not found") from e return user_model @@ -131,13 +131,13 @@ def plugin_data[**P, R]( def decorated_view(*args: P.args, **kwargs: P.kwargs) -> R: try: data = request.get_json() - except Exception: - raise ValueError("invalid json") + except Exception as e: + raise ValueError("invalid json") from e try: payload = payload_type.model_validate(data) except Exception as e: - raise ValueError(f"invalid payload: {str(e)}") + raise ValueError(f"invalid payload: {str(e)}") from e kwargs["payload"] = payload return view_func(*args, **kwargs) diff --git a/api/core/plugin/backwards_invocation/app.py b/api/core/plugin/backwards_invocation/app.py index 8e9eeb64fe0..2972e2be207 100644 --- a/api/core/plugin/backwards_invocation/app.py +++ b/api/core/plugin/backwards_invocation/app.py @@ -270,8 +270,8 @@ class PluginAppBackwardsInvocation(BaseBackwardsInvocation): app = session.scalar(select(App).where(App.id == app_id, App.tenant_id == tenant_id).limit(1)) if app: session.expunge(app) - except Exception: - raise ValueError("app not found") + except Exception as e: + raise ValueError("app not found") from e if not app: raise ValueError("app not found") diff --git a/api/core/plugin/entities/parameters.py b/api/core/plugin/entities/parameters.py index a3e59b739ad..1fed886850c 100644 --- a/api/core/plugin/entities/parameters.py +++ b/api/core/plugin/entities/parameters.py @@ -241,8 +241,10 @@ def cast_parameter_value(typ: StrEnum, value: Any, /): return str(value) except ValueError: raise - except Exception: - raise ValueError(f"The tool parameter value {repr(value)} is not in correct type of {as_normal_type(typ)}.") + except Exception as e: + raise ValueError( + f"The tool parameter value {repr(value)} is not in correct type of {as_normal_type(typ)}." + ) from e def init_frontend_parameter(rule: PluginParameter, type: StrEnum, value: Any): diff --git a/api/core/plugin/impl/base.py b/api/core/plugin/impl/base.py index 755c7362428..ce40c53c715 100644 --- a/api/core/plugin/impl/base.py +++ b/api/core/plugin/impl/base.py @@ -276,19 +276,19 @@ class BasePluginClient: json_response = transformer(json_response) # https://stackoverflow.com/questions/59634937/variable-foo-class-is-not-valid-as-type-but-why rep = PluginDaemonBasicResponse[type_].model_validate(json_response) # type: ignore - except Exception: + except Exception as e: msg = ( f"Failed to parse response from plugin daemon to PluginDaemonBasicResponse [{str(type_.__name__)}]," f" url: {path}" ) logger.exception(msg) - raise ValueError(msg) + raise ValueError(msg) from e if rep.code != 0: try: error = PluginDaemonError.model_validate(json.loads(rep.message)) - except Exception: - raise ValueError(f"{rep.message}, code: {rep.code}") + except Exception as e: + raise ValueError(f"{rep.message}, code: {rep.code}") from e self._handle_plugin_daemon_error(error.error_type, error.message) if rep.data is None: diff --git a/api/core/rag/retrieval/output_parser/structured_chat.py b/api/core/rag/retrieval/output_parser/structured_chat.py index cb5403e11d0..5d9001890e5 100644 --- a/api/core/rag/retrieval/output_parser/structured_chat.py +++ b/api/core/rag/retrieval/output_parser/structured_chat.py @@ -19,5 +19,5 @@ class StructuredChatOutputParser: return ReactAction(response["action"], response.get("action_input", {}), text) else: return ReactFinish({"output": text}, text) - except Exception: - raise ValueError(f"Could not parse LLM output: {text}") + except Exception as e: + raise ValueError(f"Could not parse LLM output: {text}") from e diff --git a/api/services/tools/api_tools_manage_service.py b/api/services/tools/api_tools_manage_service.py index 49cc1e078f0..c7a35884fa3 100644 --- a/api/services/tools/api_tools_manage_service.py +++ b/api/services/tools/api_tools_manage_service.py @@ -238,9 +238,9 @@ class ApiToolManageService: # try to parse schema, avoid SSRF attack ApiToolManageService.parser_api_schema(schema) - except Exception: + except Exception as e: logger.exception("parse api schema error") - raise ValueError("invalid schema, please check the url you provided") + raise ValueError("invalid schema, please check the url you provided") from e return {"schema": schema} @@ -453,8 +453,8 @@ class ApiToolManageService: try: tool_bundles, _ = ApiBasedToolSchemaParser.auto_parse_to_tool_bundle(schema) - except Exception: - raise ValueError("invalid schema") + except Exception as e: + raise ValueError("invalid schema") from e # get tool bundle tool_bundle = next(filter(lambda tb: tb.operation_id == tool_name, tool_bundles), None) diff --git a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py index 5faf7828f57..374c2e7ea9c 100644 --- a/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py +++ b/api/tests/unit_tests/controllers/inner_api/plugin/test_plugin_wraps.py @@ -219,8 +219,11 @@ class TestGetUser: event.listen(sqlite_plugin_engine, "before_cursor_execute", _raise_database_error) try: - with app.app_context(), pytest.raises(ValueError, match="user not found"): + with app.app_context(), pytest.raises(ValueError, match="user not found") as exc_info: get_user("tenant123", "user123") + # PEP 3134: the re-raised ValueError must chain the underlying database error. + assert isinstance(exc_info.value.__cause__, RuntimeError) + assert str(exc_info.value.__cause__) == "Database error" finally: event.remove(sqlite_plugin_engine, "before_cursor_execute", _raise_database_error) @@ -351,8 +354,11 @@ class TestPluginData: # Act & Assert - Malformed JSON triggers ValueError with app.test_request_context(data="not valid json", content_type="application/json"): - with pytest.raises(ValueError): + with pytest.raises(ValueError) as exc_info: protected_view() + # PEP 3134: the re-raised ValueError must chain the underlying JSON parse failure. + assert exc_info.value.__cause__ is not None + assert isinstance(exc_info.value.__cause__, Exception) def test_should_raise_error_on_invalid_payload(self, app: Flask): """Test that ValueError is raised when payload validation fails""" @@ -369,8 +375,11 @@ class TestPluginData: # Act & Assert with app.test_request_context(json={"data": "test"}): - with pytest.raises(ValueError, match="invalid payload"): + with pytest.raises(ValueError, match="invalid payload") as exc_info: protected_view() + # PEP 3134: the re-raised ValueError must chain the original Exception from model_validate. + assert exc_info.value.__cause__ is not None + assert str(exc_info.value.__cause__) == "Validation failed" def test_should_work_as_parameterized_decorator(self, app: Flask): """Test that decorator works when used with parentheses""" diff --git a/api/tests/unit_tests/core/plugin/test_plugin_entities.py b/api/tests/unit_tests/core/plugin/test_plugin_entities.py index acc0d0be6fa..3875ec70c5f 100644 --- a/api/tests/unit_tests/core/plugin/test_plugin_entities.py +++ b/api/tests/unit_tests/core/plugin/test_plugin_entities.py @@ -244,9 +244,15 @@ class TestPluginParameterEntities: with pytest.raises( ValueError, match=r"The tool parameter value <.*_BadString object at .* is not in correct type of string\.", - ): + ) as exc_info: cast_parameter_value(PluginParameterType.STRING, _BadString()) + # PEP 3134: the re-raised ValueError must chain the underlying RuntimeError + # so the original traceback is preserved for debugging. + assert exc_info.value.__cause__ is not None + assert isinstance(exc_info.value.__cause__, RuntimeError) + assert str(exc_info.value.__cause__) == "boom" + def test_init_frontend_parameter(self): rule = PluginParameter( name="choice", diff --git a/api/tests/unit_tests/core/rag/retrieval/test_structured_chat_output_parser.py b/api/tests/unit_tests/core/rag/retrieval/test_structured_chat_output_parser.py index c8fa0ea62fc..4de73520344 100644 --- a/api/tests/unit_tests/core/rag/retrieval/test_structured_chat_output_parser.py +++ b/api/tests/unit_tests/core/rag/retrieval/test_structured_chat_output_parser.py @@ -1,3 +1,5 @@ +import json + import pytest from core.rag.retrieval.output_parser.react_output import ReactAction, ReactFinish @@ -65,5 +67,10 @@ class TestStructuredChatOutputParser: parser = StructuredChatOutputParser() text = 'Action:\n```json\n{"action":"search","action_input": }\n```' - with pytest.raises(ValueError, match="Could not parse LLM output"): + with pytest.raises(ValueError, match="Could not parse LLM output") as exc_info: parser.parse(text) + + # PEP 3134: the re-raised ValueError must chain the underlying parse failure + # (json.JSONDecodeError on the malformed JSON) so the original traceback is preserved. + assert exc_info.value.__cause__ is not None + assert isinstance(exc_info.value.__cause__, json.JSONDecodeError)