diff --git a/framework/py/flwr/supercore/error/base.py b/framework/py/flwr/supercore/error/base.py index 96dba8e9608a..0301f8ea7c7c 100644 --- a/framework/py/flwr/supercore/error/base.py +++ b/framework/py/flwr/supercore/error/base.py @@ -16,15 +16,28 @@ from enum import IntEnum +import json class FlowerError(Exception): """Base exception that carries an internal error code and debug message.""" - def __init__(self, code: int, message: str) -> None: + def __init__( + self, code: int, message: str, public_details: str | None = None, + ) -> None: super().__init__(message) self.code = code - self.message = message + self.message = message # Sensitive message + self.public_details = public_details + + def to_json(self, public_message: str) -> str: + """Convert the error to a JSON string.""" + return json.dumps({ + "code": self.code, + "public_message": public_message, + "public_details": self.public_details, + } + ) class ApiErrorCode(IntEnum): diff --git a/framework/py/flwr/supercore/error/grpc.py b/framework/py/flwr/supercore/error/grpc.py index 48510184e9e4..f470f9973b00 100644 --- a/framework/py/flwr/supercore/error/grpc.py +++ b/framework/py/flwr/supercore/error/grpc.py @@ -17,6 +17,7 @@ from collections.abc import Iterator from contextlib import contextmanager +import json from logging import ERROR import grpc @@ -48,7 +49,7 @@ def rpc_error_translator( msg = f"[{rpc_name}][ApiError:{err.code}] {err.message}" log(ERROR, msg) - context.abort(grpc_status, public_message) + context.abort(grpc_status, err.to_json(public_message)) raise grpc.RpcError() from None # Unreachable, but satisfies type checker except Exception as err: # Let pass through if `context.abort()` is called diff --git a/framework/py/flwr/superlink/federation/noop_federation_manager.py b/framework/py/flwr/superlink/federation/noop_federation_manager.py index 305295da8ae5..56bd21add801 100644 --- a/framework/py/flwr/superlink/federation/noop_federation_manager.py +++ b/framework/py/flwr/superlink/federation/noop_federation_manager.py @@ -31,6 +31,7 @@ NOOP_FEDERATION_DESCRIPTION, ActionType, ) +import json from flwr.supercore.error import ApiErrorCode, FlowerError from flwr.supercore.typing import ActionContext @@ -47,6 +48,24 @@ def __init__(self, message: str): ) +class EntitlementError(FlowerError): + """Exception raised when an account is not entitled to perform an action.""" + + def __init__(self, details: str , entitlement_code: int): + super().__init__( + message=details, + public_details=details, + code=..., + ) + self.entitlement_code = entitlement_code + + def to_json(self) -> str: + """Convert the error into a JSON string.""" + base_dict = json.loads(super().to_json()) + base_dict["entitlement_code"] = self.entitlement_code + return json.dumps(base_dict) + + class NoOpFederationManager(FederationManager): """No-Op FederationManager implementation."""