From 18cc8956355ac97f75fc07846ae23848caf03584 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Fri, 14 Mar 2025 15:07:06 -0700 Subject: [PATCH 01/19] Improve the error handling for HTTP client so consumers can trigger appropriate behavior Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 6 +++ dapr/clients/http/client.py | 25 ++++++++-- ...t_secure_http_service_invocation_client.py | 49 +++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index e6afeaa07..ae24411b1 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -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): diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index 6f2a8e3d9..b0ab7e9ad 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -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): diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index f23bc11c1..019accf99 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -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 @@ -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')) From 4a61d218e4e7baa3fa9e508712c6d46f7db6f924 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Sun, 16 Mar 2025 10:39:27 -0700 Subject: [PATCH 02/19] PR feedback and __str__ overwrite Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 41 +++++++++++++++++++++++++++++++------ dapr/clients/http/client.py | 31 +++++++++++++--------------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index ae24411b1..a6de56506 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -33,22 +33,16 @@ 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): @@ -62,6 +56,11 @@ def as_json_safe_dict(self): return error_dict + def __str__(self): + if self._error_code != ERROR_CODE_UNKNOWN: + return f"('{self._message}', '{self._error_code}')" + return self._message or 'Unknown Dapr Error.' + class StatusDetails: def __init__(self): @@ -80,6 +79,36 @@ def as_dict(self): return {attr: getattr(self, attr) for attr in self.__dict__} +class DaprHttpError(DaprInternalError): + """DaprHttpError encapsulates all Dapr HTTP exceptions""" + def __init__( + self, + message: Optional[str] = None, + error_code: Optional[str] = ERROR_CODE_UNKNOWN, + raw_response_bytes: Optional[bytes] = None, + status_code: Optional[int] = None, + reason: Optional[str] = None, + ): + self._status_code = status_code + self._reason = reason + super(__class__, self).__init__( + message or f'HTTP status code: {status_code}', + error_code, + raw_response_bytes) + + def as_dict(self): + error_dict = super(__class__, self).as_dict() + error_dict['status_code'] = self._status_code + error_dict['reason'] = self._reason + return error_dict + + def __str__(self): + if self._error_code != ERROR_CODE_UNKNOWN: + return super(__class__, self).__str__() + else: + return f"Unknown Dapr Error. HTTP status code: {self._status_code}" + + class DaprGrpcError(RpcError): def __init__(self, err: RpcError): self._status_code = err.code() diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index b0ab7e9ad..e7835be46 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -31,7 +31,7 @@ from dapr.conf import settings from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE -from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_DOES_NOT_EXIST, ERROR_CODE_UNKNOWN +from dapr.clients.exceptions import DaprHttpError, DaprInternalError, ERROR_CODE_DOES_NOT_EXIST, ERROR_CODE_UNKNOWN class DaprHttpClient: @@ -106,39 +106,36 @@ 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( - f'HTTP status code: {response.status}', + return DaprHttpError( error_code=ERROR_CODE_DOES_NOT_EXIST, status_code=response.status, - reason=response.reason) + reason=response.reason, + ) error_info = self._serializer.deserialize(error_body) except Exception: - return DaprInternalError( - f'HTTP status code: {response.status}', + return DaprHttpError( error_code=ERROR_CODE_UNKNOWN, raw_response_bytes=error_body, status_code=response.status, - reason=response.reason + 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 - status_code = response.status - reason = response.reason - return DaprInternalError( - message or f'HTTP status code: {response.status}', - error_code, + return DaprHttpError( + message=message, + error_code=error_code, raw_response_bytes=error_body, - status_code=status_code, - reason=reason) + status_code=response.status, + reason=response.reason, + ) - return DaprInternalError( - f'HTTP status code: {response.status}', + return DaprHttpError( error_code=ERROR_CODE_UNKNOWN, raw_response_bytes=error_body, status_code=response.status, - reason=response.reason + reason=response.reason, ) def get_ssl_context(self): From 4c4917f7dedac2d0644f72934d097cea3e54d3f5 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Sun, 16 Mar 2025 11:09:00 -0700 Subject: [PATCH 03/19] cleanup Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 2 +- tests/clients/test_http_service_invocation_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index a6de56506..37704e1a6 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -106,7 +106,7 @@ def __str__(self): if self._error_code != ERROR_CODE_UNKNOWN: return super(__class__, self).__str__() else: - return f"Unknown Dapr Error. HTTP status code: {self._status_code}" + return f"Unknown Dapr Error. HTTP status code: {self._status_code}." class DaprGrpcError(RpcError): diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index d45c530ba..1e6eb2096 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -223,7 +223,7 @@ def test_invoke_method_non_dapr_error(self): error_response = b'UNPARSABLE_ERROR' self.server.set_response(error_response, 500) - expected_msg = 'Unknown Dapr Error. HTTP status code: 500' + expected_msg = 'Unknown Dapr Error. HTTP status code: 500.' with self.assertRaises(DaprInternalError) as ctx: self.client.invoke_method( From 18f06efab776d1fd8ce33d864f19f5cdc9a7eca1 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Tue, 18 Mar 2025 21:50:10 -0700 Subject: [PATCH 04/19] keep code DRY Signed-off-by: Patrick Assuied --- dapr/clients/http/client.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index e7835be46..f150c8685 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -113,12 +113,8 @@ async def convert_to_error(self, response: aiohttp.ClientResponse) -> DaprIntern ) error_info = self._serializer.deserialize(error_body) except Exception: - return DaprHttpError( - error_code=ERROR_CODE_UNKNOWN, - raw_response_bytes=error_body, - status_code=response.status, - reason=response.reason, - ) + ... + # Ignore any errors during deserialization if error_info and isinstance(error_info, dict): message = error_info.get('message') From 6e2c885cb2437cb96b91641e30edb9992a862aa0 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Tue, 18 Mar 2025 22:20:56 -0700 Subject: [PATCH 05/19] encapsulate logic in DaprHttpError constructor Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 26 ++++++++++++++-- dapr/clients/http/client.py | 30 ++----------------- ...t_secure_http_service_invocation_client.py | 12 ++++++++ 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 37704e1a6..7e7e5757e 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -14,8 +14,11 @@ """ import base64 import json -from typing import Optional +from typing import TYPE_CHECKING, Optional +if TYPE_CHECKING: + from dapr.serializers import Serializer + from google.protobuf.json_format import MessageToDict from grpc import RpcError # type: ignore from grpc_status import rpc_status # type: ignore @@ -83,14 +86,31 @@ class DaprHttpError(DaprInternalError): """DaprHttpError encapsulates all Dapr HTTP exceptions""" def __init__( self, - message: Optional[str] = None, - error_code: Optional[str] = ERROR_CODE_UNKNOWN, + serializer: 'Serializer', raw_response_bytes: Optional[bytes] = None, status_code: Optional[int] = None, reason: Optional[str] = None, ): self._status_code = status_code self._reason = reason + error_code: str = ERROR_CODE_UNKNOWN + message: Optional[str] = None + error_info: dict = None + + if (raw_response_bytes is None or len(raw_response_bytes) == 0) and status_code == 404: + error_code = ERROR_CODE_DOES_NOT_EXIST + raw_response_bytes = None + elif raw_response_bytes: + try: + error_info = serializer.deserialize(raw_response_bytes) + except Exception: + pass + # ignore any errors during deserialization + + if error_info and isinstance(error_info, dict): + message = error_info.get('message') + error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN + super(__class__, self).__init__( message or f'HTTP status code: {status_code}', error_code, diff --git a/dapr/clients/http/client.py b/dapr/clients/http/client.py index f150c8685..5944e2782 100644 --- a/dapr/clients/http/client.py +++ b/dapr/clients/http/client.py @@ -31,7 +31,7 @@ from dapr.conf import settings from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE -from dapr.clients.exceptions import DaprHttpError, DaprInternalError, ERROR_CODE_DOES_NOT_EXIST, ERROR_CODE_UNKNOWN +from dapr.clients.exceptions import DaprHttpError, DaprInternalError class DaprHttpClient: @@ -102,33 +102,9 @@ async def send_bytes( raise (await self.convert_to_error(r)) async def convert_to_error(self, response: aiohttp.ClientResponse) -> DaprInternalError: - error_info = None - try: - error_body = await response.read() - if (error_body is None or len(error_body) == 0) and response.status == 404: - return DaprHttpError( - error_code=ERROR_CODE_DOES_NOT_EXIST, - status_code=response.status, - reason=response.reason, - ) - error_info = self._serializer.deserialize(error_body) - except Exception: - ... - # Ignore any errors during deserialization - - if error_info and isinstance(error_info, dict): - message = error_info.get('message') - error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN - return DaprHttpError( - message=message, - error_code=error_code, - raw_response_bytes=error_body, - status_code=response.status, - reason=response.reason, - ) - + error_body = await response.read() return DaprHttpError( - error_code=ERROR_CODE_UNKNOWN, + self._serializer, raw_response_bytes=error_body, status_code=response.status, reason=response.reason, diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 019accf99..8e1abef3e 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -164,6 +164,18 @@ def test_notfound_no_body_exception_thrown_with_status_code_and_reason(self): 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_internal_error_no_body_exception_thrown_with_status_code_and_reason(self): + self.server.set_response(b'', code=500) + 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: 500", error_dict.get('message')) + self.assertEqual("UNKNOWN", error_dict.get('errorCode')) + self.assertEqual(b'', error_dict.get('raw_response_bytes')) + self.assertEqual(500, error_dict.get('status_code')) + self.assertEqual('Internal Server Error', 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) From 775779ebd19e771392a2c2cbc7a9e5af7fb2783e Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Tue, 18 Mar 2025 22:24:04 -0700 Subject: [PATCH 06/19] format Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 26 +++++++------- ...t_secure_http_service_invocation_client.py | 35 +++++++++++-------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 7e7e5757e..a6163549f 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from dapr.serializers import Serializer - + from google.protobuf.json_format import MessageToDict from grpc import RpcError # type: ignore from grpc_status import rpc_status # type: ignore @@ -84,12 +84,13 @@ def as_dict(self): class DaprHttpError(DaprInternalError): """DaprHttpError encapsulates all Dapr HTTP exceptions""" + def __init__( - self, - serializer: 'Serializer', - raw_response_bytes: Optional[bytes] = None, - status_code: Optional[int] = None, - reason: Optional[str] = None, + self, + serializer: 'Serializer', + raw_response_bytes: Optional[bytes] = None, + status_code: Optional[int] = None, + reason: Optional[str] = None, ): self._status_code = status_code self._reason = reason @@ -112,22 +113,21 @@ def __init__( error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN super(__class__, self).__init__( - message or f'HTTP status code: {status_code}', - error_code, - raw_response_bytes) - + message or f'HTTP status code: {status_code}', error_code, raw_response_bytes + ) + def as_dict(self): error_dict = super(__class__, self).as_dict() error_dict['status_code'] = self._status_code error_dict['reason'] = self._reason return error_dict - + def __str__(self): if self._error_code != ERROR_CODE_UNKNOWN: return super(__class__, self).__str__() else: - return f"Unknown Dapr Error. HTTP status code: {self._status_code}." - + return f'Unknown Dapr Error. HTTP status code: {self._status_code}.' + class DaprGrpcError(RpcError): def __init__(self, err: RpcError): diff --git a/tests/clients/test_secure_http_service_invocation_client.py b/tests/clients/test_secure_http_service_invocation_client.py index 8e1abef3e..4d1bdda1f 100644 --- a/tests/clients/test_secure_http_service_invocation_client.py +++ b/tests/clients/test_secure_http_service_invocation_client.py @@ -147,8 +147,8 @@ def test_notfound_json_body_exception_thrown_with_status_code_and_reason(self): 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('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')) @@ -159,8 +159,8 @@ def test_notfound_no_body_exception_thrown_with_status_code_and_reason(self): 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('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')) @@ -171,32 +171,37 @@ def test_internal_error_no_body_exception_thrown_with_status_code_and_reason(sel self.client.invoke_method(self.app_id, self.method_name, '') error_dict = context.exception.as_dict() - self.assertEqual("HTTP status code: 500", error_dict.get('message')) - self.assertEqual("UNKNOWN", error_dict.get('errorCode')) + self.assertEqual('HTTP status code: 500', error_dict.get('message')) + self.assertEqual('UNKNOWN', error_dict.get('errorCode')) self.assertEqual(b'', error_dict.get('raw_response_bytes')) self.assertEqual(500, error_dict.get('status_code')) self.assertEqual('Internal Server Error', 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) + 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('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) + 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('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')) From cb2dfe31cee8f1d70c1f6f9b642adeaaf0d91935 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Thu, 20 Mar 2025 12:44:48 -0700 Subject: [PATCH 07/19] Apply suggestions from code review Co-authored-by: Elena Kolevska Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index a6163549f..832214281 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -83,7 +83,12 @@ def as_dict(self): class DaprHttpError(DaprInternalError): - """DaprHttpError encapsulates all Dapr HTTP exceptions""" + """DaprHttpError encapsulates all Dapr HTTP exceptions + + Attributes: + _status_code: HTTP status code + _reason: HTTP reason phrase + """ def __init__( self, @@ -112,19 +117,24 @@ def __init__( message = error_info.get('message') error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN - super(__class__, self).__init__( - message or f'HTTP status code: {status_code}', error_code, raw_response_bytes - ) + super().__init__(message or f'HTTP status code: {status_code}', error_code, + raw_response_bytes) + @property + def status_code(self) -> Optional[int]: + return self._status_code + @property + def reason(self) -> Optional[str]: + return self._reason def as_dict(self): - error_dict = super(__class__, self).as_dict() + error_dict = super().as_dict() error_dict['status_code'] = self._status_code error_dict['reason'] = self._reason return error_dict def __str__(self): if self._error_code != ERROR_CODE_UNKNOWN: - return super(__class__, self).__str__() + return f"{self._message} (Error Code: {self._error_code}, Status Code: {self._status_code})" else: return f'Unknown Dapr Error. HTTP status code: {self._status_code}.' From abe3e0178e12f02ae12b5acf6b6ce2efdaf993ff Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Thu, 20 Mar 2025 12:52:22 -0700 Subject: [PATCH 08/19] update test following applying suggestions Signed-off-by: Patrick Assuied --- tests/clients/test_http_service_invocation_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index 1e6eb2096..4167405b7 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -205,10 +205,10 @@ def test_invoke_method_protobuf_response_case_insensitive(self): self.assertEqual('resp', new_resp.key) def test_invoke_method_error_returned(self): - error_response = b'{"errorCode":"ERR_DIRECT_INVOKE","message":"Something bad happend"}' + error_response = b'{"errorCode":"ERR_DIRECT_INVOKE","message":"Something bad happened"}' self.server.set_response(error_response, 500) - expected_msg = "('Something bad happend', 'ERR_DIRECT_INVOKE')" + expected_msg = "Something bad happened (Error Code: ERR_DIRECT_INVOKE, Status Code: 500)" with self.assertRaises(DaprInternalError) as ctx: self.client.invoke_method( From 1e66ff45b388002b7765ab2948ec844e8f7f33c8 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Thu, 20 Mar 2025 12:58:52 -0700 Subject: [PATCH 09/19] Apply suggestions from code review Co-authored-by: Elena Kolevska Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 832214281..c8104565a 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -58,7 +58,20 @@ def as_json_safe_dict(self): ) return error_dict + @property + def message(self) -> Optional[str]: + """Get the error message""" + return self._message + + @property + def error_code(self) -> Optional[str]: + """Get the error code""" + return self._error_code + @property + def raw_response_bytes(self) -> Optional[bytes]: + """Get the raw response bytes""" + return self._raw_response_bytes def __str__(self): if self._error_code != ERROR_CODE_UNKNOWN: return f"('{self._message}', '{self._error_code}')" From 17f688aa6186a6ad3185c1ea3eecc336b9a97cab Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Thu, 20 Mar 2025 13:05:28 -0700 Subject: [PATCH 10/19] format Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index c8104565a..9413a8d35 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -58,6 +58,7 @@ def as_json_safe_dict(self): ) return error_dict + @property def message(self) -> Optional[str]: """Get the error message""" @@ -72,6 +73,7 @@ def error_code(self) -> Optional[str]: def raw_response_bytes(self) -> Optional[bytes]: """Get the raw response bytes""" return self._raw_response_bytes + def __str__(self): if self._error_code != ERROR_CODE_UNKNOWN: return f"('{self._message}', '{self._error_code}')" @@ -130,8 +132,12 @@ def __init__( message = error_info.get('message') error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN - super().__init__(message or f'HTTP status code: {status_code}', error_code, - raw_response_bytes) + super().__init__( + message or f'HTTP status code: {status_code}', + error_code, + raw_response_bytes + ) + @property def status_code(self) -> Optional[int]: return self._status_code @@ -139,6 +145,7 @@ def status_code(self) -> Optional[int]: @property def reason(self) -> Optional[str]: return self._reason + def as_dict(self): error_dict = super().as_dict() error_dict['status_code'] = self._status_code From 8cbbecc10507a28af4087193dfd765e9197cc13b Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Fri, 21 Mar 2025 09:30:46 -0700 Subject: [PATCH 11/19] format Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 6 ++---- tests/clients/test_http_service_invocation_client.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 9413a8d35..9ea29dea9 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -133,9 +133,7 @@ def __init__( error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN super().__init__( - message or f'HTTP status code: {status_code}', - error_code, - raw_response_bytes + message or f'HTTP status code: {status_code}', error_code, raw_response_bytes ) @property @@ -154,7 +152,7 @@ def as_dict(self): def __str__(self): if self._error_code != ERROR_CODE_UNKNOWN: - return f"{self._message} (Error Code: {self._error_code}, Status Code: {self._status_code})" + return f'{self._message} (Error Code: {self._error_code}, Status Code: {self._status_code})' else: return f'Unknown Dapr Error. HTTP status code: {self._status_code}.' diff --git a/tests/clients/test_http_service_invocation_client.py b/tests/clients/test_http_service_invocation_client.py index 4167405b7..c0b43a863 100644 --- a/tests/clients/test_http_service_invocation_client.py +++ b/tests/clients/test_http_service_invocation_client.py @@ -208,7 +208,7 @@ def test_invoke_method_error_returned(self): error_response = b'{"errorCode":"ERR_DIRECT_INVOKE","message":"Something bad happened"}' self.server.set_response(error_response, 500) - expected_msg = "Something bad happened (Error Code: ERR_DIRECT_INVOKE, Status Code: 500)" + expected_msg = 'Something bad happened (Error Code: ERR_DIRECT_INVOKE, Status Code: 500)' with self.assertRaises(DaprInternalError) as ctx: self.client.invoke_method( From adad0322593d77d07b581a9e404d078751adfe0c Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Fri, 21 Mar 2025 11:27:13 -0700 Subject: [PATCH 12/19] Updated example Signed-off-by: Patrick Assuied --- examples/invoke-http/README.md | 3 +++ examples/invoke-http/invoke-caller.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/invoke-http/README.md b/examples/invoke-http/README.md index 08e8738e6..466392d20 100644 --- a/examples/invoke-http/README.md +++ b/examples/invoke-http/README.md @@ -51,6 +51,9 @@ expected_stdout_lines: - '== APP == 200' - '== APP == error occurred' - '== APP == MY_CODE' + - '== APP == {"message": "error occurred", "errorCode": "MY_CODE"}' + - '== APP == 503' + - '== APP == Internal Server Error' background: true sleep: 5 --> diff --git a/examples/invoke-http/invoke-caller.py b/examples/invoke-http/invoke-caller.py index ebc5876b9..380001592 100644 --- a/examples/invoke-http/invoke-caller.py +++ b/examples/invoke-http/invoke-caller.py @@ -2,6 +2,7 @@ import time from dapr.clients import DaprClient +from dapr.clients.exceptions import DaprHttpError with DaprClient() as d: req_data = {'id': 1, 'message': 'hello world'} @@ -29,6 +30,9 @@ http_verb='POST', data=json.dumps(req_data), ) - except Exception as e: - print(e._message, flush=True) - print(e._error_code, flush=True) + except DaprHttpError as e: + print(e.message, flush=True) + print(e.error_code, flush=True) + print(e.raw_response_bytes, flush=True) + print(str(e.status_code), flush=True) + print(e.reason, flush=True) From 220b641aaed704d315fa4fc9c99e5737b559fdb8 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Thu, 27 Mar 2025 17:28:31 -0700 Subject: [PATCH 13/19] fix typing Signed-off-by: Patrick Assuied --- dapr/clients/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapr/clients/exceptions.py b/dapr/clients/exceptions.py index 9ea29dea9..61ae0d8b6 100644 --- a/dapr/clients/exceptions.py +++ b/dapr/clients/exceptions.py @@ -116,7 +116,7 @@ def __init__( self._reason = reason error_code: str = ERROR_CODE_UNKNOWN message: Optional[str] = None - error_info: dict = None + error_info: Optional[dict] = None if (raw_response_bytes is None or len(raw_response_bytes) == 0) and status_code == 404: error_code = ERROR_CODE_DOES_NOT_EXIST From 248e19fc997170b219cfde45be8e957f7ff11229 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Mon, 14 Apr 2025 10:29:38 -0700 Subject: [PATCH 14/19] Upgrade flake8 and fix found issue Signed-off-by: Patrick Assuied --- dev-requirements.txt | 2 +- ext/dapr-ext-workflow/tests/test_workflow_client.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index cec56fb2a..4c3ddd2d3 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ mypy>=1.2.0 mypy-extensions>=0.4.3 mypy-protobuf>=2.9 -flake8>=3.7.9 +flake8>=7.2.0 tox>=4.3.0 coverage>=5.3 wheel diff --git a/ext/dapr-ext-workflow/tests/test_workflow_client.py b/ext/dapr-ext-workflow/tests/test_workflow_client.py index 984c97cdf..540c0e801 100644 --- a/ext/dapr-ext-workflow/tests/test_workflow_client.py +++ b/ext/dapr-ext-workflow/tests/test_workflow_client.py @@ -57,7 +57,6 @@ def schedule_new_orchestration( return mock_schedule_result def get_orchestration_state(self, instance_id, fetch_payloads): - global wf_status if wf_status == 'not-found': raise SimulatedRpcError(code='UNKNOWN', details='no such instance exists') elif wf_status == 'found': From 6ca18437eec33d2f0245d449befb99f352a775ea Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Mon, 14 Apr 2025 13:05:58 -0700 Subject: [PATCH 15/19] Upgrade deps with vulnerabilities: - fastapi - grpcio - aiohttp - uvicorn Signed-off-by: Patrick Assuied --- ext/dapr-ext-fastapi/setup.cfg | 4 ++-- setup.cfg | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ext/dapr-ext-fastapi/setup.cfg b/ext/dapr-ext-fastapi/setup.cfg index 560a795f8..d4ee278f6 100644 --- a/ext/dapr-ext-fastapi/setup.cfg +++ b/ext/dapr-ext-fastapi/setup.cfg @@ -25,8 +25,8 @@ packages = find_namespace: include_package_data = True install_requires = dapr-dev >= 1.15.0.dev - uvicorn >= 0.11.6 - fastapi >= 0.60.1 + uvicorn >= 0.11.7 + fastapi >= 0.109.1 [options.packages.find] include = diff --git a/setup.cfg b/setup.cfg index de5d53f4f..0950a07e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,9 +26,9 @@ include_package_data = True zip_safe = False install_requires = protobuf >= 4.22 - grpcio >= 1.37.0 + grpcio >= 1.53.2 grpcio-status>=1.37.0 - aiohttp >= 3.9.0b0 + aiohttp >= 3.10.11 python-dateutil >= 2.8.1 typing-extensions>=4.4.0 From 1fa0274f18d9b55a128cd4a322bbed0ed7d90f9f Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Mon, 14 Apr 2025 13:45:34 -0700 Subject: [PATCH 16/19] upgrade pyOpenSSL Signed-off-by: Patrick Assuied --- dev-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 4c3ddd2d3..ba5e3d325 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,7 +9,7 @@ wheel opentelemetry-sdk opentelemetry-instrumentation-grpc httpx>=0.24 -pyOpenSSL>=23.2.0 +pyOpenSSL>=23.3.0 # needed for type checking Flask>=1.1 # needed for auto fix From cdf97c283bb2336853185f69f6294faf2c99406d Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Mon, 14 Apr 2025 13:52:54 -0700 Subject: [PATCH 17/19] Forcing upgrade of cryptography Signed-off-by: Patrick Assuied --- dev-requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index ba5e3d325..194328fd2 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -9,7 +9,8 @@ wheel opentelemetry-sdk opentelemetry-instrumentation-grpc httpx>=0.24 -pyOpenSSL>=23.3.0 +pyOpenSSL>=24.3.0 +cryptography>=44.0.1 # needed for type checking Flask>=1.1 # needed for auto fix From e01dcaf8ecf0d35b51fb609dbd0ef2b1a4177a77 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Mon, 14 Apr 2025 15:49:02 -0700 Subject: [PATCH 18/19] Revert "Forcing upgrade of cryptography" Revert "upgrade pyOpenSSL" Revert "Upgrade deps with vulnerabilities:" Revert "Upgrade flake8 and fix found issue" Signed-off-by: Patrick Assuied --- dev-requirements.txt | 5 ++--- ext/dapr-ext-fastapi/setup.cfg | 4 ++-- ext/dapr-ext-workflow/tests/test_workflow_client.py | 1 + setup.cfg | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 194328fd2..cec56fb2a 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,7 +1,7 @@ mypy>=1.2.0 mypy-extensions>=0.4.3 mypy-protobuf>=2.9 -flake8>=7.2.0 +flake8>=3.7.9 tox>=4.3.0 coverage>=5.3 wheel @@ -9,8 +9,7 @@ wheel opentelemetry-sdk opentelemetry-instrumentation-grpc httpx>=0.24 -pyOpenSSL>=24.3.0 -cryptography>=44.0.1 +pyOpenSSL>=23.2.0 # needed for type checking Flask>=1.1 # needed for auto fix diff --git a/ext/dapr-ext-fastapi/setup.cfg b/ext/dapr-ext-fastapi/setup.cfg index d4ee278f6..560a795f8 100644 --- a/ext/dapr-ext-fastapi/setup.cfg +++ b/ext/dapr-ext-fastapi/setup.cfg @@ -25,8 +25,8 @@ packages = find_namespace: include_package_data = True install_requires = dapr-dev >= 1.15.0.dev - uvicorn >= 0.11.7 - fastapi >= 0.109.1 + uvicorn >= 0.11.6 + fastapi >= 0.60.1 [options.packages.find] include = diff --git a/ext/dapr-ext-workflow/tests/test_workflow_client.py b/ext/dapr-ext-workflow/tests/test_workflow_client.py index 540c0e801..984c97cdf 100644 --- a/ext/dapr-ext-workflow/tests/test_workflow_client.py +++ b/ext/dapr-ext-workflow/tests/test_workflow_client.py @@ -57,6 +57,7 @@ def schedule_new_orchestration( return mock_schedule_result def get_orchestration_state(self, instance_id, fetch_payloads): + global wf_status if wf_status == 'not-found': raise SimulatedRpcError(code='UNKNOWN', details='no such instance exists') elif wf_status == 'found': diff --git a/setup.cfg b/setup.cfg index 0950a07e6..de5d53f4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,9 +26,9 @@ include_package_data = True zip_safe = False install_requires = protobuf >= 4.22 - grpcio >= 1.53.2 + grpcio >= 1.37.0 grpcio-status>=1.37.0 - aiohttp >= 3.10.11 + aiohttp >= 3.9.0b0 python-dateutil >= 2.8.1 typing-extensions>=4.4.0 From 96ee49c750b0c33ef201f50bbe045ce565548f78 Mon Sep 17 00:00:00 2001 From: Patrick Assuied Date: Mon, 14 Apr 2025 15:53:52 -0700 Subject: [PATCH 19/19] remove global var. lint issue Signed-off-by: Patrick Assuied --- ext/dapr-ext-workflow/tests/test_workflow_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ext/dapr-ext-workflow/tests/test_workflow_client.py b/ext/dapr-ext-workflow/tests/test_workflow_client.py index 984c97cdf..540c0e801 100644 --- a/ext/dapr-ext-workflow/tests/test_workflow_client.py +++ b/ext/dapr-ext-workflow/tests/test_workflow_client.py @@ -57,7 +57,6 @@ def schedule_new_orchestration( return mock_schedule_result def get_orchestration_state(self, instance_id, fetch_payloads): - global wf_status if wf_status == 'not-found': raise SimulatedRpcError(code='UNKNOWN', details='no such instance exists') elif wf_status == 'found':