From 4672bde471c4ba8b31778b064cc1ee6747031ceb Mon Sep 17 00:00:00 2001 From: Mahalaxmi Date: Sun, 1 Nov 2026 19:33:20 +0000 Subject: [PATCH 1/2] fix(google-api-core): make RetryError picklable and copyable RetryError.__init__ requires a 'cause' argument that isn't stored in self.args, so the default BaseException.__reduce__ rebuilds it as RetryError(message) and fails with: TypeError: RetryError.__init__() missing 1 required positional argument: 'cause' This breaks pickling (e.g. returning the error from multiprocessing or concurrent.futures workers) as well as copy.copy/copy.deepcopy. Define __reduce__ so the exception is rebuilt with both message and cause. --- .../google/api_core/exceptions.py | 6 +++ .../tests/unit/test_exceptions.py | 37 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/google-api-core/google/api_core/exceptions.py b/packages/google-api-core/google/api_core/exceptions.py index aa9898c5b9a6..03f5ac62bbac 100644 --- a/packages/google-api-core/google/api_core/exceptions.py +++ b/packages/google-api-core/google/api_core/exceptions.py @@ -93,6 +93,12 @@ def cause(self): def __str__(self): return "{}, last exception: {}".format(self.message, self.cause) + def __reduce__(self): + # ``cause`` is a required constructor argument that isn't stored in + # ``self.args``, so the default ``BaseException.__reduce__`` can't + # rebuild the exception (e.g. when it's pickled across processes). + return (self.__class__, (self.message, self._cause)) + class _GoogleAPICallErrorMeta(type): """Metaclass for registering GoogleAPICallError subclasses.""" diff --git a/packages/google-api-core/tests/unit/test_exceptions.py b/packages/google-api-core/tests/unit/test_exceptions.py index 6a71ddeb9261..10db96f9bf7f 100644 --- a/packages/google-api-core/tests/unit/test_exceptions.py +++ b/packages/google-api-core/tests/unit/test_exceptions.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import http.client import json +import pickle from unittest import mock import pytest @@ -54,6 +56,41 @@ def test_create_google_cloud_error_with_args(): assert exception.response == response +@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) +def test_retry_error_pickle_round_trip(protocol): + cause = exceptions.ServiceUnavailable("backend unavailable") + error = exceptions.RetryError("Timeout of 1.0s exceeded", cause) + + restored = pickle.loads(pickle.dumps(error, protocol=protocol)) + + assert isinstance(restored, exceptions.RetryError) + assert restored.message == error.message + assert isinstance(restored.cause, exceptions.ServiceUnavailable) + assert restored.cause.message == cause.message + assert str(restored) == str(error) + + +def test_retry_error_pickle_round_trip_without_cause(): + error = exceptions.RetryError("Timeout of 1.0s exceeded", None) + + restored = pickle.loads(pickle.dumps(error)) + + assert restored.cause is None + assert str(restored) == str(error) + + +@pytest.mark.parametrize("copy_fn", [copy.copy, copy.deepcopy]) +def test_retry_error_copy(copy_fn): + cause = exceptions.ServiceUnavailable("backend unavailable") + error = exceptions.RetryError("Timeout of 1.0s exceeded", cause) + + copied = copy_fn(error) + + assert isinstance(copied, exceptions.RetryError) + assert copied.message == error.message + assert str(copied) == str(error) + + def test_from_http_status(): message = "message" exception = exceptions.from_http_status(http.client.NOT_FOUND, message) From 30407fbaf5ec10212844ebf2742dfd75886d2047 Mon Sep 17 00:00:00 2001 From: Mahalaxmi Date: Sun, 1 Nov 2026 19:41:11 +0000 Subject: [PATCH 2/2] fix(google-api-core): preserve RetryError instance attributes when pickling Return self.__dict__ as the state from RetryError.__reduce__, matching BaseException's default behavior, so attributes set on the instance (including notes added with add_note()) survive pickling and copying. --- packages/google-api-core/google/api_core/exceptions.py | 2 +- packages/google-api-core/tests/unit/test_exceptions.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/exceptions.py b/packages/google-api-core/google/api_core/exceptions.py index 03f5ac62bbac..7235da756d30 100644 --- a/packages/google-api-core/google/api_core/exceptions.py +++ b/packages/google-api-core/google/api_core/exceptions.py @@ -97,7 +97,7 @@ def __reduce__(self): # ``cause`` is a required constructor argument that isn't stored in # ``self.args``, so the default ``BaseException.__reduce__`` can't # rebuild the exception (e.g. when it's pickled across processes). - return (self.__class__, (self.message, self._cause)) + return (self.__class__, (self.message, self._cause), self.__dict__) class _GoogleAPICallErrorMeta(type): diff --git a/packages/google-api-core/tests/unit/test_exceptions.py b/packages/google-api-core/tests/unit/test_exceptions.py index 10db96f9bf7f..04eaefbc0e7b 100644 --- a/packages/google-api-core/tests/unit/test_exceptions.py +++ b/packages/google-api-core/tests/unit/test_exceptions.py @@ -79,6 +79,15 @@ def test_retry_error_pickle_round_trip_without_cause(): assert str(restored) == str(error) +def test_retry_error_pickle_preserves_instance_attributes(): + error = exceptions.RetryError("Timeout of 1.0s exceeded", None) + error.custom_attribute = "value" + + restored = pickle.loads(pickle.dumps(error)) + + assert restored.custom_attribute == "value" + + @pytest.mark.parametrize("copy_fn", [copy.copy, copy.deepcopy]) def test_retry_error_copy(copy_fn): cause = exceptions.ServiceUnavailable("backend unavailable")