Skip to content

Commit 027b9b6

Browse files
Merge branch 'main' into patch-1
2 parents af9deff + 0256265 commit 027b9b6

File tree

7 files changed

+169
-28
lines changed

7 files changed

+169
-28
lines changed

dapr/clients/exceptions.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
"""
1515
import base64
1616
import json
17-
from typing import Optional
17+
from typing import TYPE_CHECKING, Optional
18+
19+
if TYPE_CHECKING:
20+
from dapr.serializers import Serializer
1821

1922
from google.protobuf.json_format import MessageToDict
2023
from grpc import RpcError # type: ignore
@@ -56,6 +59,26 @@ def as_json_safe_dict(self):
5659

5760
return error_dict
5861

62+
@property
63+
def message(self) -> Optional[str]:
64+
"""Get the error message"""
65+
return self._message
66+
67+
@property
68+
def error_code(self) -> Optional[str]:
69+
"""Get the error code"""
70+
return self._error_code
71+
72+
@property
73+
def raw_response_bytes(self) -> Optional[bytes]:
74+
"""Get the raw response bytes"""
75+
return self._raw_response_bytes
76+
77+
def __str__(self):
78+
if self._error_code != ERROR_CODE_UNKNOWN:
79+
return f"('{self._message}', '{self._error_code}')"
80+
return self._message or 'Unknown Dapr Error.'
81+
5982

6083
class StatusDetails:
6184
def __init__(self):
@@ -74,6 +97,66 @@ def as_dict(self):
7497
return {attr: getattr(self, attr) for attr in self.__dict__}
7598

7699

100+
class DaprHttpError(DaprInternalError):
101+
"""DaprHttpError encapsulates all Dapr HTTP exceptions
102+
103+
Attributes:
104+
_status_code: HTTP status code
105+
_reason: HTTP reason phrase
106+
"""
107+
108+
def __init__(
109+
self,
110+
serializer: 'Serializer',
111+
raw_response_bytes: Optional[bytes] = None,
112+
status_code: Optional[int] = None,
113+
reason: Optional[str] = None,
114+
):
115+
self._status_code = status_code
116+
self._reason = reason
117+
error_code: str = ERROR_CODE_UNKNOWN
118+
message: Optional[str] = None
119+
error_info: Optional[dict] = None
120+
121+
if (raw_response_bytes is None or len(raw_response_bytes) == 0) and status_code == 404:
122+
error_code = ERROR_CODE_DOES_NOT_EXIST
123+
raw_response_bytes = None
124+
elif raw_response_bytes:
125+
try:
126+
error_info = serializer.deserialize(raw_response_bytes)
127+
except Exception:
128+
pass
129+
# ignore any errors during deserialization
130+
131+
if error_info and isinstance(error_info, dict):
132+
message = error_info.get('message')
133+
error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN
134+
135+
super().__init__(
136+
message or f'HTTP status code: {status_code}', error_code, raw_response_bytes
137+
)
138+
139+
@property
140+
def status_code(self) -> Optional[int]:
141+
return self._status_code
142+
143+
@property
144+
def reason(self) -> Optional[str]:
145+
return self._reason
146+
147+
def as_dict(self):
148+
error_dict = super().as_dict()
149+
error_dict['status_code'] = self._status_code
150+
error_dict['reason'] = self._reason
151+
return error_dict
152+
153+
def __str__(self):
154+
if self._error_code != ERROR_CODE_UNKNOWN:
155+
return f'{self._message} (Error Code: {self._error_code}, Status Code: {self._status_code})'
156+
else:
157+
return f'Unknown Dapr Error. HTTP status code: {self._status_code}.'
158+
159+
77160
class DaprGrpcError(RpcError):
78161
def __init__(self, err: RpcError):
79162
self._status_code = err.code()

dapr/clients/http/client.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
from dapr.conf import settings
3333
from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE
34-
from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_DOES_NOT_EXIST, ERROR_CODE_UNKNOWN
34+
from dapr.clients.exceptions import DaprHttpError, DaprInternalError
3535

3636

3737
class DaprHttpClient:
@@ -102,26 +102,12 @@ async def send_bytes(
102102
raise (await self.convert_to_error(r))
103103

104104
async def convert_to_error(self, response: aiohttp.ClientResponse) -> DaprInternalError:
105-
error_info = None
106-
try:
107-
error_body = await response.read()
108-
if (error_body is None or len(error_body) == 0) and response.status == 404:
109-
return DaprInternalError('Not Found', ERROR_CODE_DOES_NOT_EXIST)
110-
error_info = self._serializer.deserialize(error_body)
111-
except Exception:
112-
return DaprInternalError(
113-
f'Unknown Dapr Error. HTTP status code: {response.status}',
114-
raw_response_bytes=error_body,
115-
)
116-
117-
if error_info and isinstance(error_info, dict):
118-
message = error_info.get('message')
119-
error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN
120-
return DaprInternalError(message, error_code, raw_response_bytes=error_body)
121-
122-
return DaprInternalError(
123-
f'Unknown Dapr Error. HTTP status code: {response.status}',
105+
error_body = await response.read()
106+
return DaprHttpError(
107+
self._serializer,
124108
raw_response_bytes=error_body,
109+
status_code=response.status,
110+
reason=response.reason,
125111
)
126112

127113
def get_ssl_context(self):

examples/invoke-http/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ expected_stdout_lines:
5151
- '== APP == 200'
5252
- '== APP == error occurred'
5353
- '== APP == MY_CODE'
54+
- '== APP == {"message": "error occurred", "errorCode": "MY_CODE"}'
55+
- '== APP == 503'
56+
- '== APP == Internal Server Error'
5457
background: true
5558
sleep: 5
5659
-->

examples/invoke-http/invoke-caller.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import time
33

44
from dapr.clients import DaprClient
5+
from dapr.clients.exceptions import DaprHttpError
56

67
with DaprClient() as d:
78
req_data = {'id': 1, 'message': 'hello world'}
@@ -29,6 +30,9 @@
2930
http_verb='POST',
3031
data=json.dumps(req_data),
3132
)
32-
except Exception as e:
33-
print(e._message, flush=True)
34-
print(e._error_code, flush=True)
33+
except DaprHttpError as e:
34+
print(e.message, flush=True)
35+
print(e.error_code, flush=True)
36+
print(e.raw_response_bytes, flush=True)
37+
print(str(e.status_code), flush=True)
38+
print(e.reason, flush=True)

ext/dapr-ext-workflow/tests/test_workflow_client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def schedule_new_orchestration(
5757
return mock_schedule_result
5858

5959
def get_orchestration_state(self, instance_id, fetch_payloads):
60-
global wf_status
6160
if wf_status == 'not-found':
6261
raise SimulatedRpcError(code='UNKNOWN', details='no such instance exists')
6362
elif wf_status == 'found':

tests/clients/test_http_service_invocation_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,10 @@ def test_invoke_method_protobuf_response_case_insensitive(self):
205205
self.assertEqual('resp', new_resp.key)
206206

207207
def test_invoke_method_error_returned(self):
208-
error_response = b'{"errorCode":"ERR_DIRECT_INVOKE","message":"Something bad happend"}'
208+
error_response = b'{"errorCode":"ERR_DIRECT_INVOKE","message":"Something bad happened"}'
209209
self.server.set_response(error_response, 500)
210210

211-
expected_msg = "('Something bad happend', 'ERR_DIRECT_INVOKE')"
211+
expected_msg = 'Something bad happened (Error Code: ERR_DIRECT_INVOKE, Status Code: 500)'
212212

213213
with self.assertRaises(DaprInternalError) as ctx:
214214
self.client.invoke_method(
@@ -223,7 +223,7 @@ def test_invoke_method_non_dapr_error(self):
223223
error_response = b'UNPARSABLE_ERROR'
224224
self.server.set_response(error_response, 500)
225225

226-
expected_msg = 'Unknown Dapr Error. HTTP status code: 500'
226+
expected_msg = 'Unknown Dapr Error. HTTP status code: 500.'
227227

228228
with self.assertRaises(DaprInternalError) as ctx:
229229
self.client.invoke_method(

tests/clients/test_secure_http_service_invocation_client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
2424

2525
from dapr.clients import DaprClient, DaprGrpcClient
26+
from dapr.clients.exceptions import DaprInternalError
2627
from dapr.clients.health import DaprHealth
2728
from dapr.clients.http.client import DaprHttpClient
2829
from dapr.conf import settings
@@ -139,3 +140,68 @@ def test_timeout_exception_thrown_when_timeout_reached(self):
139140
self.server.set_server_delay(1.5)
140141
with self.assertRaises(TimeoutError):
141142
new_client.invoke_method(self.app_id, self.method_name, '')
143+
144+
def test_notfound_json_body_exception_thrown_with_status_code_and_reason(self):
145+
self.server.set_response(b'{"error": "Not found"}', code=404)
146+
with self.assertRaises(DaprInternalError) as context:
147+
self.client.invoke_method(self.app_id, self.method_name, '')
148+
149+
error_dict = context.exception.as_dict()
150+
self.assertEqual('HTTP status code: 404', error_dict.get('message'))
151+
self.assertEqual('UNKNOWN', error_dict.get('errorCode'))
152+
self.assertEqual(b'{"error": "Not found"}', error_dict.get('raw_response_bytes'))
153+
self.assertEqual(404, error_dict.get('status_code'))
154+
self.assertEqual('Not Found', error_dict.get('reason'))
155+
156+
def test_notfound_no_body_exception_thrown_with_status_code_and_reason(self):
157+
self.server.set_response(b'', code=404)
158+
with self.assertRaises(DaprInternalError) as context:
159+
self.client.invoke_method(self.app_id, self.method_name, '')
160+
161+
error_dict = context.exception.as_dict()
162+
self.assertEqual('HTTP status code: 404', error_dict.get('message'))
163+
self.assertEqual('ERR_DOES_NOT_EXIST', error_dict.get('errorCode'))
164+
self.assertEqual(None, error_dict.get('raw_response_bytes'))
165+
self.assertEqual(404, error_dict.get('status_code'))
166+
self.assertEqual('Not Found', error_dict.get('reason'))
167+
168+
def test_internal_error_no_body_exception_thrown_with_status_code_and_reason(self):
169+
self.server.set_response(b'', code=500)
170+
with self.assertRaises(DaprInternalError) as context:
171+
self.client.invoke_method(self.app_id, self.method_name, '')
172+
173+
error_dict = context.exception.as_dict()
174+
self.assertEqual('HTTP status code: 500', error_dict.get('message'))
175+
self.assertEqual('UNKNOWN', error_dict.get('errorCode'))
176+
self.assertEqual(b'', error_dict.get('raw_response_bytes'))
177+
self.assertEqual(500, error_dict.get('status_code'))
178+
self.assertEqual('Internal Server Error', error_dict.get('reason'))
179+
180+
def test_notfound_no_json_body_exception_thrown_with_status_code_and_reason(self):
181+
self.server.set_response(b'Not found', code=404)
182+
with self.assertRaises(DaprInternalError) as context:
183+
self.client.invoke_method(self.app_id, self.method_name, '')
184+
185+
error_dict = context.exception.as_dict()
186+
self.assertEqual('HTTP status code: 404', error_dict.get('message'))
187+
self.assertEqual('UNKNOWN', error_dict.get('errorCode'))
188+
self.assertEqual(b'Not found', error_dict.get('raw_response_bytes'))
189+
self.assertEqual(404, error_dict.get('status_code'))
190+
self.assertEqual('Not Found', error_dict.get('reason'))
191+
192+
def test_notfound_json_body_w_message_exception_thrown_with_status_code_and_reason(self):
193+
self.server.set_response(
194+
b'{"message": "My message", "errorCode": "MY_ERROR_CODE"}', code=404
195+
)
196+
with self.assertRaises(DaprInternalError) as context:
197+
self.client.invoke_method(self.app_id, self.method_name, '')
198+
199+
error_dict = context.exception.as_dict()
200+
self.assertEqual('My message', error_dict.get('message'))
201+
self.assertEqual('MY_ERROR_CODE', error_dict.get('errorCode'))
202+
self.assertEqual(
203+
b'{"message": "My message", "errorCode": "MY_ERROR_CODE"}',
204+
error_dict.get('raw_response_bytes'),
205+
)
206+
self.assertEqual(404, error_dict.get('status_code'))
207+
self.assertEqual('Not Found', error_dict.get('reason'))

0 commit comments

Comments
 (0)