Skip to content

Commit 0f6e96a

Browse files
Add DaprInternalError.as_json_safe_dict for actors (dapr#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 <[email protected]> Co-authored-by: Elena Kolevska <[email protected]>
1 parent ec48779 commit 0f6e96a

File tree

4 files changed

+54
-14
lines changed

4 files changed

+54
-14
lines changed

dapr/clients/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
See the License for the specific language governing permissions and
1313
limitations under the License.
1414
"""
15+
import base64
1516
import json
1617
from typing import Optional
1718

@@ -44,6 +45,17 @@ def as_dict(self):
4445
'raw_response_bytes': self._raw_response_bytes,
4546
}
4647

48+
def as_json_safe_dict(self):
49+
error_dict = self.as_dict()
50+
51+
if self._raw_response_bytes is not None:
52+
# Encode bytes to base64 for JSON compatibility
53+
error_dict['raw_response_bytes'] = base64.b64encode(self._raw_response_bytes).decode(
54+
'utf-8'
55+
)
56+
57+
return error_dict
58+
4759

4860
class StatusDetails:
4961
def __init__(self):

ext/dapr-ext-fastapi/dapr/ext/fastapi/actor.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@
1515

1616
from typing import Any, Optional, Type, List
1717

18+
from dapr.actor import Actor, ActorRuntime
19+
from dapr.clients.exceptions import ERROR_CODE_UNKNOWN, DaprInternalError
20+
from dapr.serializers import DefaultJSONSerializer
1821
from fastapi import FastAPI, APIRouter, Request, Response, status # type: ignore
1922
from fastapi.logger import logger
2023
from fastapi.responses import JSONResponse
2124

22-
from dapr.actor import Actor, ActorRuntime
23-
from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_UNKNOWN
24-
from dapr.serializers import DefaultJSONSerializer
25-
2625
DEFAULT_CONTENT_TYPE = 'application/json; utf-8'
2726
DAPR_REENTRANCY_ID_HEADER = 'Dapr-Reentrancy-Id'
2827

@@ -72,7 +71,7 @@ async def actor_deactivation(actor_type_name: str, actor_id: str):
7271
try:
7372
await ActorRuntime.deactivate(actor_type_name, actor_id)
7473
except DaprInternalError as ex:
75-
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict())
74+
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict())
7675
except Exception as ex:
7776
return _wrap_response(
7877
status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN
@@ -96,7 +95,7 @@ async def actor_method(
9695
actor_type_name, actor_id, method_name, req_body, reentrancy_id
9796
)
9897
except DaprInternalError as ex:
99-
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict())
98+
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict())
10099
except Exception as ex:
101100
return _wrap_response(
102101
status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN
@@ -117,7 +116,7 @@ async def actor_timer(
117116
req_body = await request.body()
118117
await ActorRuntime.fire_timer(actor_type_name, actor_id, timer_name, req_body)
119118
except DaprInternalError as ex:
120-
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict())
119+
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict())
121120
except Exception as ex:
122121
return _wrap_response(
123122
status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN
@@ -139,7 +138,7 @@ async def actor_reminder(
139138
req_body = await request.body()
140139
await ActorRuntime.fire_reminder(actor_type_name, actor_id, reminder_name, req_body)
141140
except DaprInternalError as ex:
142-
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_dict())
141+
return _wrap_response(status.HTTP_500_INTERNAL_SERVER_ERROR, ex.as_json_safe_dict())
143142
except Exception as ex:
144143
return _wrap_response(
145144
status.HTTP_500_INTERNAL_SERVER_ERROR, repr(ex), ERROR_CODE_UNKNOWN

ext/flask_dapr/flask_dapr/actor.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from flask import jsonify, make_response, request
2020

2121
from dapr.actor import Actor, ActorRuntime
22-
from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_UNKNOWN
22+
from dapr.clients.exceptions import ERROR_CODE_UNKNOWN, DaprInternalError
2323
from dapr.serializers import DefaultJSONSerializer
2424

2525
DEFAULT_CONTENT_TYPE = 'application/json; utf-8'
@@ -80,7 +80,7 @@ def _deactivation_handler(self, actor_type_name, actor_id):
8080
try:
8181
asyncio.run(ActorRuntime.deactivate(actor_type_name, actor_id))
8282
except DaprInternalError as ex:
83-
return wrap_response(500, ex.as_dict())
83+
return wrap_response(500, ex.as_json_safe_dict())
8484
except Exception as ex:
8585
return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN)
8686

@@ -99,7 +99,7 @@ def _method_handler(self, actor_type_name, actor_id, method_name):
9999
)
100100
)
101101
except DaprInternalError as ex:
102-
return wrap_response(500, ex.as_dict())
102+
return wrap_response(500, ex.as_json_safe_dict())
103103
except Exception as ex:
104104
return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN)
105105

@@ -113,7 +113,7 @@ def _timer_handler(self, actor_type_name, actor_id, timer_name):
113113
req_body = request.stream.read()
114114
asyncio.run(ActorRuntime.fire_timer(actor_type_name, actor_id, timer_name, req_body))
115115
except DaprInternalError as ex:
116-
return wrap_response(500, ex.as_dict())
116+
return wrap_response(500, ex.as_json_safe_dict())
117117
except Exception as ex:
118118
return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN)
119119

@@ -129,7 +129,7 @@ def _reminder_handler(self, actor_type_name, actor_id, reminder_name):
129129
ActorRuntime.fire_reminder(actor_type_name, actor_id, reminder_name, req_body)
130130
)
131131
except DaprInternalError as ex:
132-
return wrap_response(500, ex.as_dict())
132+
return wrap_response(500, ex.as_json_safe_dict())
133133
except Exception as ex:
134134
return wrap_response(500, repr(ex), ERROR_CODE_UNKNOWN)
135135

tests/clients/test_exceptions.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import base64
2+
import json
13
import unittest
24

35
import grpc
@@ -6,7 +8,7 @@
68
from google.protobuf.duration_pb2 import Duration
79

810
from dapr.clients import DaprGrpcClient
9-
from dapr.clients.exceptions import DaprGrpcError
11+
from dapr.clients.exceptions import DaprGrpcError, DaprInternalError
1012
from dapr.conf import settings
1113

1214
from .fake_dapr_server import FakeDaprSidecar
@@ -216,3 +218,30 @@ def test_error_code(self):
216218
dapr_error = context.exception
217219

218220
self.assertEqual(dapr_error.error_code(), 'UNKNOWN')
221+
222+
def test_dapr_internal_error_as_json_safe_dict_no_bytes(self):
223+
message = 'Test DaprInternalError.as_json_safe_dict with no raw bytes'
224+
dapr_error = DaprInternalError(message=message)
225+
226+
safe_dict = dapr_error.as_json_safe_dict()
227+
self.assertEqual(safe_dict['message'], message)
228+
self.assertEqual(safe_dict['errorCode'], 'UNKNOWN')
229+
self.assertIsNone(safe_dict['raw_response_bytes'])
230+
231+
# Also check that the safe dict can be serialised to JSON
232+
_ = json.dumps(safe_dict)
233+
234+
def test_dapr_internal_error_as_json_safe_dict_bytes_are_encoded(self):
235+
message = 'Test DaprInternalError.as_json_safe_dict with encoded raw bytes'
236+
raw_bytes = message.encode('utf-8')
237+
dapr_error = DaprInternalError(message=message, raw_response_bytes=raw_bytes)
238+
239+
safe_dict = dapr_error.as_json_safe_dict()
240+
self.assertEqual(safe_dict['message'], message)
241+
self.assertEqual(safe_dict['errorCode'], 'UNKNOWN')
242+
243+
decoded_bytes = base64.b64decode(safe_dict['raw_response_bytes'])
244+
self.assertEqual(decoded_bytes, raw_bytes)
245+
246+
# Also check that the safe dict can be serialised to JSON
247+
_ = json.dumps(safe_dict)

0 commit comments

Comments
 (0)