From 0f6e96a3164b5cdbc3ca8888fa60e5729b3e8a4e Mon Sep 17 00:00:00 2001 From: Billy Brown Date: Thu, 9 Jan 2025 17:14:57 +0000 Subject: [PATCH] Add DaprInternalError.as_json_safe_dict for actors (#765) The FastAPI and Flask extensions for actors serialise the value of any raised DaprInternalError to JSON, which fails if the error contains bytes in its `_raw_response_bytes` field. This change adds a new `as_json_safe_dict` method and uses it in place of the `as_dict` method in the FastAPI and Flask extensions. Two unit tests for the `as_json_safe_dict` method are included. Signed-off-by: Billy Brown Co-authored-by: Elena Kolevska --- dapr/clients/exceptions.py | 12 +++++++ .../dapr/ext/fastapi/actor.py | 15 +++++---- ext/flask_dapr/flask_dapr/actor.py | 10 +++--- tests/clients/test_exceptions.py | 31 ++++++++++++++++++- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 91bc04a8..e6afeaa0 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -12,6 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +import base64 import json from typing import Optional @@ -44,6 +45,17 @@ def as_dict(self): 'raw_response_bytes': self._raw_response_bytes, } + def as_json_safe_dict(self): + error_dict = self.as_dict() + + if self._raw_response_bytes is not None: + # Encode bytes to base64 for JSON compatibility + error_dict['raw_response_bytes'] = base64.b64encode(self._raw_response_bytes).decode( + 'utf-8' + ) + + return error_dict + class StatusDetails: def __init__(self): diff --git a/ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py b/ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py index 793704f7..93b7860e 100644 --- a/ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py +++ b/ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py @@ -15,14 +15,13 @@ from typing import Any, Optional, Type, List +from dapr.actor import Actor, ActorRuntime +from dapr.clients.exceptions import ERROR_CODE_UNKNOWN, DaprInternalError +from dapr.serializers import DefaultJSONSerializer from fastapi import FastAPI, APIRouter, Request, Response, status # type: ignore from fastapi.logger import logger from fastapi.responses import JSONResponse -from dapr.actor import Actor, ActorRuntime -from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_UNKNOWN -from dapr.serializers import DefaultJSONSerializer - DEFAULT_CONTENT_TYPE = 'application/json; utf-8' DAPR_REENTRANCY_ID_HEADER = 'Dapr-Reentrancy-Id' @@ -72,7 +71,7 @@ async def actor_deactivation(actor_type_name: str, actor_id: str): try: await ActorRuntime.deactivate(actor_type_name, actor_id) except DaprInternalError as ex: - return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict()) + return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict()) except Exception as ex: return _wrap_response( status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN @@ -96,7 +95,7 @@ async def actor_method( actor_type_name, actor_id, method_name, req_body, reentrancy_id ) except DaprInternalError as ex: - return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict()) + return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict()) except Exception as ex: return _wrap_response( status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN @@ -117,7 +116,7 @@ async def actor_timer( req_body = await request.body() await ActorRuntime.fire_timer(actor_type_name, actor_id, timer_name, req_body) except DaprInternalError as ex: - return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict()) + return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict()) except Exception as ex: return _wrap_response( status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN @@ -139,7 +138,7 @@ async def actor_reminder( req_body = await request.body() await ActorRuntime.fire_reminder(actor_type_name, actor_id, reminder_name, req_body) except DaprInternalError as ex: - return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict()) + return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict()) except Exception as ex: return _wrap_response( status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN diff --git a/ext/flask_dapr/flask_dapr/actor.py b/ext/flask_dapr/flask_dapr/actor.py index b717de15..c8bdf1a5 100644 --- a/ext/flask_dapr/flask_dapr/actor.py +++ b/ext/flask_dapr/flask_dapr/actor.py @@ -19,7 +19,7 @@ from flask import jsonify, make_response, request from dapr.actor import Actor, ActorRuntime -from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_UNKNOWN +from dapr.clients.exceptions import ERROR_CODE_UNKNOWN, DaprInternalError from dapr.serializers import DefaultJSONSerializer DEFAULT_CONTENT_TYPE = 'application/json; utf-8' @@ -80,7 +80,7 @@ def _deactivation_handler(self, actor_type_name, actor_id): try: asyncio.run(ActorRuntime.deactivate(actor_type_name, actor_id)) except DaprInternalError as ex: - return wrap_response(500, ex.as_dict()) + return wrap_response(500, ex.as_json_safe_dict()) except Exception as ex: return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN) @@ -99,7 +99,7 @@ def _method_handler(self, actor_type_name, actor_id, method_name): ) ) except DaprInternalError as ex: - return wrap_response(500, ex.as_dict()) + return wrap_response(500, ex.as_json_safe_dict()) except Exception as ex: return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN) @@ -113,7 +113,7 @@ def _timer_handler(self, actor_type_name, actor_id, timer_name): req_body = request.stream.read() asyncio.run(ActorRuntime.fire_timer(actor_type_name, actor_id, timer_name, req_body)) except DaprInternalError as ex: - return wrap_response(500, ex.as_dict()) + return wrap_response(500, ex.as_json_safe_dict()) except Exception as ex: return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN) @@ -129,7 +129,7 @@ def _reminder_handler(self, actor_type_name, actor_id, reminder_name): ActorRuntime.fire_reminder(actor_type_name, actor_id, reminder_name, req_body) ) except DaprInternalError as ex: - return wrap_response(500, ex.as_dict()) + return wrap_response(500, ex.as_json_safe_dict()) except Exception as ex: return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN) diff --git a/tests/clients/test_exceptions.py b/tests/clients/test_exceptions.py index fb349b09..08eea4d5 100644 --- a/tests/clients/test_exceptions.py +++ b/tests/clients/test_exceptions.py @@ -1,3 +1,5 @@ +import base64 +import json import unittest import grpc @@ -6,7 +8,7 @@ from google.protobuf.duration_pb2 import Duration from dapr.clients import DaprGrpcClient -from dapr.clients.exceptions import DaprGrpcError +from dapr.clients.exceptions import DaprGrpcError, DaprInternalError from dapr.conf import settings from .fake_dapr_server import FakeDaprSidecar @@ -216,3 +218,30 @@ def test_error_code(self): dapr_error = context.exception self.assertEqual(dapr_error.error_code(), 'UNKNOWN') + + def test_dapr_internal_error_as_json_safe_dict_no_bytes(self): + message = 'Test DaprInternalError.as_json_safe_dict with no raw bytes' + dapr_error = DaprInternalError(message=message) + + safe_dict = dapr_error.as_json_safe_dict() + self.assertEqual(safe_dict['message'], message) + self.assertEqual(safe_dict['errorCode'], 'UNKNOWN') + self.assertIsNone(safe_dict['raw_response_bytes']) + + # Also check that the safe dict can be serialised to JSON + _ = json.dumps(safe_dict) + + def test_dapr_internal_error_as_json_safe_dict_bytes_are_encoded(self): + message = 'Test DaprInternalError.as_json_safe_dict with encoded raw bytes' + raw_bytes = message.encode('utf-8') + dapr_error = DaprInternalError(message=message, raw_response_bytes=raw_bytes) + + safe_dict = dapr_error.as_json_safe_dict() + self.assertEqual(safe_dict['message'], message) + self.assertEqual(safe_dict['errorCode'], 'UNKNOWN') + + decoded_bytes = base64.b64decode(safe_dict['raw_response_bytes']) + self.assertEqual(decoded_bytes, raw_bytes) + + # Also check that the safe dict can be serialised to JSON + _ = json.dumps(safe_dict)