Exceptions are formatted incorrectly (or even fail) when `message` or `detail` attribute presents
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'
Looks like this special handling of message attribute is from Python 2 world, as BaseException.message was deprecated in Python 2.6 and removed in 2.7 and 3.0 (see PEP 352 for details). So the fix in #2193 (adds special handling of detail) to solve #2192, doesn't look correct to me. Instead, we should have removed special handling of message attribute. As you can see, the exception from example in #2192 is formatted correctly by standard tools, but misses status code when formatted by sentry:
fapi_exc = fastapi.HTTPException(429, detail="Too Many Requests")
print("".join(traceback.format_exception_only(fapi_exc)).strip())
# fastapi.exceptions.HTTPException: 429: Too Many Requests
print(sentry_sdk.utils.get_error_message(fapi_exc))
# `status_code` field is missing:
# Too Many Requests
The special case for exception with a detail field seems to have been added because Starlette did not have a __str__ method on their exceptions until recently: https://github.com/Kludex/starlette/commit/2168e47052239da5df35d5353bb986f760c51cef.
https://github.com/getsentry/sentry-python/blob/5b055894042e0bc68a7945a5b8cb43b975b407fc/sentry_sdk/utils.py#L655-L661
Hi @ods,
We'll fix the unhandled exception you have reported first, and remove special handling of the message and detail attributes in the next major version. Thanks again for the research!