-
Notifications
You must be signed in to change notification settings - Fork 96
Open
Description
Environment details
- OS type and version: macOS 26.1
- Python version: 3.12.12
- pip version: 25.3
google-api-coreversion: HEAD = f8bf6f9610f3e0e7580f223794c3906513e1fa73
Description
The retry mechanism clears the __cause__ attribute when re-raising non-retryable exceptions, breaking explicit exception chaining created with raise ... from ....
Steps to reproduce
- Use the
@retry_async.AsyncRetrydecorator - Raise a non-retryable exception with a cause
- Catch the exception and try to read or log its cause
Code example
import asyncio
from google.api_core import retry_async, exceptions
@retry_async.AsyncRetry(predicate=retry_async.if_exception_type(exceptions.InternalServerError))
async def example():
try:
raise exceptions.Unauthenticated("Invalid credentials")
except exceptions.Unauthenticated as exc:
raise ValueError("Access denied") from exc # Explicit chaining
async def main():
# After going through retry:
try:
await example()
except ValueError as e:
print(e.__cause__) # Expected: "401 Invalid credentials"; Actual: None
if __name__ == "__main__":
asyncio.run(main())See https://github.com/googleapis/python-api-core/pull/879/changes for a unit test that fails with the current code and passes with the proposed fix.
Root Cause
In retry_base.py:168, _default_exception_factory() returns (exc_list[-1], None), which causes raise final_exc from None on line 214, explicitly clearing __cause__.
Proposed fix
Preserve the exception's cause attribute:
# Before
return exc_list[-1], None
# After
final_exc = exc_list[-1]
cause = getattr(final_exc, '__cause__', None)
return final_exc, causeThis maintains exception chains for better debugging and meets developer expectations for exception handling.
Draft pull request
Metadata
Metadata
Assignees
Labels
No labels