Repository navigation
[Bug]: Preserve empty errors through core decoding and persistence #880
Description
Expected behavior
Preserve a present Error: {} through core decoding and persistence while treating it as absent diagnostic information when projecting plugin data or building exception messages. Omitted or null errors remain absent. Explicit fields such as {"ErrorMessage": ""} and {"StackTrace": []} must retain their values.
Actual behavior
At core SDK 3.0.1 / PR #870 head 1eb341b1054af2a9a25ad6907ca64d522b909bdd, CallbackDetails.from_dict, StepDetails.from_dict, ContextDetails.from_dict, and ChainedInvokeDetails.from_dict collapse Error: {} to None. Directly constructed empty errors therefore differ from decoded state.
An earlier revision of #870 (58567dc) preserved empty errors, but that change was reverted. If an empty ErrorObject is retained, it is truthy: plugin.py::_extract_error() exposes it to plugins, and CheckpointedResult.raise_operation_error() bypasses the missing-error fallback and raises a typed operation exception with message=None.
Steps to reproduce
Run against the core package at the revision above:
from aws_durable_execution_sdk_python.exceptions import StepError
from aws_durable_execution_sdk_python.lambda_service import ErrorObject, StepDetails
from aws_durable_execution_sdk_python.state import CheckpointedResult
print(StepDetails.from_dict({"Error": {}}).error)
# None: the present empty object was discarded.
try:
CheckpointedResult(error=ErrorObject.from_dict({})).raise_operation_error(StepError)
except StepError as error:
print(repr(error.message))
# None: retaining the empty object currently bypasses the fallback.Locally reproduced with Python 3.13.5 and core SDK 3.0.1. All four detail decoders dropped the empty object. Direct consumer checks also reproduced empty plugin diagnostics and message=None for step, child-context, and invoke exceptions. No claim is made that this is a newly introduced regression.
Scope and proposed implementation
- Preserve
Error: {}in the four detail decoders and throughOperationwire/persisted-state round trips; retain omitted/null behavior. - In
plugin.py::_extract_error(), expose no diagnostic error for an object with no fields, without changing the checkpointed object. - In
CheckpointedResult.raise_operation_error(), use the existing missing-error fallback whenerror is None or not error.to_dict(). Retain the correct operation exception class and caller-supplied fallback behavior. - Preserve all explicit non-null fields, including empty strings and empty stack-trace lists.
- Align testing-library callback completion and history reconstruction with preservation so those boundaries do not discard the core state this change intends to retain.
- Keep detailed history payload presence and metadata-only/truncation behavior intact.
Do not change ErrorObject truthiness globally. Keep the preservation decision in storage conversion and the diagnostic decision at the consumers.
Acceptance criteria
- Parameterized tests cover omitted, null, empty, partially populated, explicit empty-valued, and rich errors for all four detail types.
- A present empty error survives operation serialization and persistence; omitted/null errors remain absent.
- Plugin operation snapshots and terminal information omit wholly empty diagnostics, preserve supplied fields, and do not mutate checkpoint state.
- Failed step, child-context, and invoke replay raises the correct exception class with the existing fallback for wholly absent diagnostics.
- Public callback tests cover omitted/empty/rich failures with memory and filesystem stores, before persistence and after resume.
- Initial invocation and replay/resume preserve failure meaning, skip completed work, and retain rich error message/type/data/stack fields.
- Focused regressions fail before the fix; core/testing checks and relevant public-handler tests pass afterward.
Relationship to OTel and existing issues
This is the separate preservation follow-up requested in the #870 review discussion. It differs from #862's original approach, which normalizes empty errors to absent SDK state; reconcile that part of #862 when implementing this issue.
OTel failure-status mapping, suppression of exception events without diagnostics, and event timestamps remain in #853 / PR #865. Keep those changes focused on the OTel package; this core preservation work should not be folded into that PR.
Deployed Lambda/service representation is unverified by this local reproduction. The review discussion supplies service/parity context; retain that distinction when documenting validation and coordinate any shared conformance changes separately.
Activity