From b64703fae93b31b1ab5c1db9b15478c5266b86f8 Mon Sep 17 00:00:00 2001 From: wsj Date: Sun, 25 Oct 2026 10:58:54 +0800 Subject: [PATCH] fix(result): avoid mutating errors when pickling task results --- taskiq/result/result.py | 7 ++++--- tests/test_result.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/taskiq/result/result.py b/taskiq/result/result.py index f995242c..b0f6cdc9 100644 --- a/taskiq/result/result.py +++ b/taskiq/result/result.py @@ -55,8 +55,9 @@ def raise_for_error(self) -> "Self": return self def __getstate__(self) -> dict[Any, Any]: - dict = super().__getstate__() - vals: builtins.dict[str, Any] = dict["__dict__"] + state = super().__getstate__() + vals: builtins.dict[str, Any] = state["__dict__"].copy() + state["__dict__"] = vals if "error" in vals and vals["error"] is not None: vals["error"] = prepare_exception( @@ -64,7 +65,7 @@ def __getstate__(self) -> dict[Any, Any]: pickle, ) - return dict + return state @field_validator("error", mode="before") @classmethod diff --git a/tests/test_result.py b/tests/test_result.py index 1633c318..51716e50 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -64,6 +64,30 @@ def test_pickle_error_serialization() -> None: assert type(data.error) == type(task.error) # noqa: E721 +def test_pickle_unpickleable_error_does_not_mutate_result() -> None: + class LocalError(Exception): + pass + + error = LocalError("boom") + result: TaskiqResult[None] = TaskiqResult( + is_err=True, + return_value=None, + execution_time=0, + error=error, + ) + + serialized = pickle.dumps(result) + + restored = pickle.loads(serialized) + assert restored.is_err + assert isinstance(restored.error, BaseException) + assert str(restored.error) == repr(error) + assert result.error is error + with pytest.raises(LocalError) as exc: + result.raise_for_error() + assert exc.value is error + + def test_result_raise_for_error_exc() -> None: error = ValueError("Error") res: TaskiqResult[None] = TaskiqResult(