Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the error handling for HTTP client so consumers can trigger appropriate behavior #793

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions dapr/clients/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,22 @@ def __init__(
message: Optional[str],
error_code: Optional[str] = ERROR_CODE_UNKNOWN,
raw_response_bytes: Optional[bytes] = None,
status_code: Optional[int] = None,
reason: Optional[str] = None,
):
self._message = message
self._error_code = error_code
self._raw_response_bytes = raw_response_bytes
self._status_code = status_code
self._reason = reason

def as_dict(self):
return {
'message': self._message,
'errorCode': self._error_code,
'raw_response_bytes': self._raw_response_bytes,
'status_code': self._status_code,
'reason': self._reason,
}

def as_json_safe_dict(self):
Expand Down
25 changes: 21 additions & 4 deletions dapr/clients/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,22 +106,39 @@ async def convert_to_error(self, response: aiohttp.ClientResponse) -> DaprIntern
try:
error_body = await response.read()
if (error_body is None or len(error_body) == 0) and response.status == 404:
return DaprInternalError('Not Found', ERROR_CODE_DOES_NOT_EXIST)
return DaprInternalError(
f'HTTP status code: {response.status}',
error_code=ERROR_CODE_DOES_NOT_EXIST,
status_code=response.status,
reason=response.reason)
error_info = self._serializer.deserialize(error_body)
except Exception:
return DaprInternalError(
f'Unknown Dapr Error. HTTP status code: {response.status}',
f'HTTP status code: {response.status}',
error_code=ERROR_CODE_UNKNOWN,
raw_response_bytes=error_body,
status_code=response.status,
reason=response.reason
)

if error_info and isinstance(error_info, dict):
message = error_info.get('message')
error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN
return DaprInternalError(message, error_code, raw_response_bytes=error_body)
status_code = response.status
reason = response.reason
return DaprInternalError(
message or f'HTTP status code: {response.status}',
error_code,
raw_response_bytes=error_body,
status_code=status_code,
reason=reason)

return DaprInternalError(
f'Unknown Dapr Error. HTTP status code: {response.status}',
f'HTTP status code: {response.status}',
error_code=ERROR_CODE_UNKNOWN,
raw_response_bytes=error_body,
status_code=response.status,
reason=response.reason
)

def get_ssl_context(self):
Expand Down
49 changes: 49 additions & 0 deletions tests/clients/test_secure_http_service_invocation_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator

from dapr.clients import DaprClient, DaprGrpcClient
from dapr.clients.exceptions import DaprInternalError
from dapr.clients.health import DaprHealth
from dapr.clients.http.client import DaprHttpClient
from dapr.conf import settings
Expand Down Expand Up @@ -139,3 +140,51 @@ def test_timeout_exception_thrown_when_timeout_reached(self):
self.server.set_server_delay(1.5)
with self.assertRaises(TimeoutError):
new_client.invoke_method(self.app_id, self.method_name, '')

def test_notfound_json_body_exception_thrown_with_status_code_and_reason(self):
self.server.set_response(b'{"error": "Not found"}', code=404)
with self.assertRaises(DaprInternalError) as context:
self.client.invoke_method(self.app_id, self.method_name, '')

error_dict = context.exception.as_dict()
self.assertEqual("HTTP status code: 404", error_dict.get('message'))
self.assertEqual("UNKNOWN", error_dict.get('errorCode'))
self.assertEqual(b'{"error": "Not found"}', error_dict.get('raw_response_bytes'))
self.assertEqual(404, error_dict.get('status_code'))
self.assertEqual('Not Found', error_dict.get('reason'))

def test_notfound_no_body_exception_thrown_with_status_code_and_reason(self):
self.server.set_response(b'', code=404)
with self.assertRaises(DaprInternalError) as context:
self.client.invoke_method(self.app_id, self.method_name, '')

error_dict = context.exception.as_dict()
self.assertEqual("HTTP status code: 404", error_dict.get('message'))
self.assertEqual("ERR_DOES_NOT_EXIST", error_dict.get('errorCode'))
self.assertEqual(None, error_dict.get('raw_response_bytes'))
self.assertEqual(404, error_dict.get('status_code'))
self.assertEqual('Not Found', error_dict.get('reason'))

def test_notfound_no_json_body_exception_thrown_with_status_code_and_reason(self):
self.server.set_response(b"Not found", code=404)
with self.assertRaises(DaprInternalError) as context:
self.client.invoke_method(self.app_id, self.method_name, '')

error_dict = context.exception.as_dict()
self.assertEqual("HTTP status code: 404", error_dict.get('message'))
self.assertEqual("UNKNOWN", error_dict.get('errorCode'))
self.assertEqual(b"Not found", error_dict.get('raw_response_bytes'))
self.assertEqual(404, error_dict.get('status_code'))
self.assertEqual('Not Found', error_dict.get('reason'))

def test_notfound_json_body_w_message_exception_thrown_with_status_code_and_reason(self):
self.server.set_response(b'{"message": "My message", "errorCode": "MY_ERROR_CODE"}', code=404)
with self.assertRaises(DaprInternalError) as context:
self.client.invoke_method(self.app_id, self.method_name, '')

error_dict = context.exception.as_dict()
self.assertEqual("My message", error_dict.get('message'))
self.assertEqual("MY_ERROR_CODE", error_dict.get('errorCode'))
self.assertEqual(b'{"message": "My message", "errorCode": "MY_ERROR_CODE"}', error_dict.get('raw_response_bytes'))
self.assertEqual(404, error_dict.get('status_code'))
self.assertEqual('Not Found', error_dict.get('reason'))