Skip to content

Exceptions are formatted incorrectly (or even fail) when message or detail attribute presents #5050

@ods

Description

@ods

How do you use Sentry?

Self-hosted/on-premise

Version

2.43.0

Steps to Reproduce

Some of our exception define properties message and detail. The formatting of error message for those exceptions is almost always incorrect (doesn't contain important information, not formatted properly) or sometimes even fails. There is a couple of such exceptions in simplified form:

class GRpcError(Exception):
    def __init__(self, code: int, message: bytes) -> None:
        super().__init__(code, message)
        self.code = code
        # Original protobuf message to allow its inspection in error handlers.
        # Here we use bytes, but it can be `google.protobuf.message.Message`
        # instance as well.
        self.message = message

    def __str__(self) -> str:
        return f"code={self.code}"


class ApiError(Exception):
    def __init__(self, code: str, detail: dict[str, Any]) -> None:
        super().__init__(code, detail)
        self.code = code
        self.detail = detail

    def __str__(self) -> str:
        formatted = f"[{self.code}]"
        for key, value in self.detail.items():
            formatted += f"\n  {key}={value}"
        return formatted

Expected Result

Here is how they are formatted by Python itself:

grpc_exc = GRpcError(13, b"unreadable protobuf message")

print("".join(traceback.format_exception_only(grpc_exc)).strip())
# GRpcError: code=13

grpc_exc.add_note("additional context note")

print("".join(traceback.format_exception_only(grpc_exc)).strip())
# GRpcError: code=13
# additional context note

api_exc = ApiError("RATE_LIMIT_EXCEEDED", detail={"retry_after": 30})

print("".join(traceback.format_exception_only(api_exc)).strip())
# ApiError: [RATE_LIMIT_EXCEEDED]
#   retry_after=30

api_exc.add_note("additional context note")

print("".join(traceback.format_exception_only(api_exc)).strip())
# ApiError: [RATE_LIMIT_EXCEEDED]
#   retry_after=30
# additional context note

Actual Result

And the formatting by sentry (get_error_message is used internally by event_from_exception, here I call it directly):

grpc_exc = GRpcError(13, b"unreadable protobuf message")

print(sentry_sdk.utils.get_error_message(grpc_exc))
# No `code` field, only unreadable field in the result:
# b'unreadable protobuf message'

grpc_exc.add_note("additional context note")

print(sentry_sdk.utils.get_error_message(grpc_exc))
# Fails with the error:
# TypeError: can't concat str to bytes

api_exc = ApiError("RATE_LIMIT_EXCEEDED", detail={"retry_after": 30})

print(sentry_sdk.utils.get_error_message(api_exc))
# No `code` field, `detail` is not formatted:
# {'retry_after': 30}

api_exc.add_note("additional context note")

print(sentry_sdk.utils.get_error_message(api_exc))
# Fails with the error:
# TypeError: unsupported operand type(s) for +=: 'dict' and 'str'

Metadata

Metadata

Projects

Status

Waiting for: Product Owner

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions