Skip to content

Commit

Permalink
Add DaprInternalError.as_json_safe_dict for actors (#765)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Elena Kolevska <[email protected]>
  • Loading branch information
Druid-of-Luhn and elena-kolevska authored Jan 9, 2025
1 parent ec48779 commit 0f6e96a
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 14 deletions.
12 changes: 12 additions & 0 deletions dapr/clients/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
15 changes: 7 additions & 8 deletions ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions ext/flask_dapr/flask_dapr/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -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)

Expand Down
31 changes: 30 additions & 1 deletion tests/clients/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import base64
import json
import unittest

import grpc
Expand All @@ -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
Expand Down Expand Up @@ -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)

0 comments on commit 0f6e96a

Please sign in to comment.