dapr / python-sdk

Dapr SDK for Python
Apache License 2.0
222 stars 125 forks source link

[FEATURE REQUEST] Improved developer experience for error handling #648

Closed elena-kolevska closed 7 months ago

elena-kolevska commented 9 months ago

Describe the feature

Developers would have to do some gymnastics (example below) in their apps in order to get to the error details in the new, richer error model (https://github.com/dapr/dapr/pull/7257). I suggest we add a helper function in the SDK to improve the developer experience, at the very minimum - provide an equivalent of the parse_grpc_error function below.

Here's what developers need to do, currently:

def parse_grpc_error(error):
    if not isinstance(error, grpc.RpcError):
        return None

    error_status = {
        'code': error.code(),
        'message': error.details(),
        'details': []
    }

    # Check if the error has additional details
    if error.trailing_metadata():
        for metadata in error.trailing_metadata():
            if metadata.key == 'grpc-status-details-bin':
                status_proto = status_pb2.Status()
                status_proto.MergeFromString(metadata.value)
                for detail in status_proto.details:
                    any_pb = any_pb2.Any()
                    any_pb.CopyFrom(detail)
                    # You can add specific parsing for known types here
                    error_status['details'].append(any_pb)

    return error_status

with DaprClient() as d:

    storeName = 'statestore1'

    key = "key_1||"
    value = "value_1"

    # Wait for sidecar to be up within 5 seconds.
    d.wait(2)

    # Save single state.
    try:
        d.save_state(store_name=storeName, key=key, value=value)
        print(f"State store has successfully saved {value} with {key} as key")
    except grpc.RpcError as err:
        error_status = parse_grpc_error(err)

        print("Error code: ", error_status['code'])
        print("Error message: ", error_status['message'])

        for detail in error_status['details']:
            detail_any = any_pb2.Any()
            detail_any.CopyFrom(detail)

            # Check and handle each expected type
            if detail_any.Is(error_details_pb2.ErrorInfo.DESCRIPTOR):
                error_info = error_details_pb2.ErrorInfo()
                detail_any.Unpack(error_info)
                print("ErrorInfo:", error_info)
            elif detail_any.Is(error_details_pb2.ResourceInfo.DESCRIPTOR):
                resource_info = error_details_pb2.ResourceInfo()
                detail_any.Unpack(resource_info)
                print("ResourceInfo:", resource_info)
            elif detail_any.Is(error_details_pb2.BadRequest.DESCRIPTOR):
                bad_request = error_details_pb2.BadRequest()
                detail_any.Unpack(bad_request)
                print("BadRequest:", bad_request)
            # Add more elif blocks for other types you expect
            else:
                print("Unknown detail type")

Release Note

RELEASE NOTE: UPDATE Improved developer experience for error handling

berndverst commented 9 months ago

The new error model is not merged in runtime yet - so from the SDK perspective it does not exist yet. PRs welcome once there is a runtime RC that contains the new error model.

berndverst commented 8 months ago

How about a new subclass of type grpc.RpcError with all the convenience methods you need to make it easy to extract standardized Dapr error in a convenient way. This would be the most user friendly.

Of course everywhere in the code such as in save_state() you need to catch the grpc.RpcError and use that to instantiate your new error class. Then throw the new error class instead.

elena-kolevska commented 8 months ago

Yes, I was thinking the same. I'll start on it this week.

elena-kolevska commented 8 months ago

/assign