Skip to content

[Bug]: Type-only callback errors append ': null' to caller and telemetry messages #849

Closed

Description

@zhongkechen

Expected Behavior

A callback failed with only ErrorType: "RejectedError" must not produce the literal text "null" as if it were an error message. The caller-facing exception should have a useful type-only display message, and telemetry records should not publish RejectedError: null.

Preserve the original ErrorObject, including the absence of ErrorMessage, and retain the existing callback exception class and retry/replay behavior.

Actual Behavior

CallbackFailedException.buildMessage concatenates errorType + ": " + errorMessage whenever the type is present, including when the message is null. Consequently, a type-only external failure produces:

RejectedError: null

The core-generated message also reaches Durable Execution Telemetry. Its nameAndMessage helper reads the checkpointed ErrorObject when available, but falls back to Throwable.getMessage() when ErrorMessage is absent. An uncaught CallbackFailedException therefore produces a telemetry error whose name can correctly be RejectedError while its message is still RejectedError: null. A containing operation that checkpoints that caller error can carry the malformed message into later snapshots as well.

This affects error records sent to the configured telemetry destinations; it is not limited to OTel exception events. The callback leaf's original error type can already be preserved by the telemetry exporter, so do not conflate this message-formatting defect with #847.

Steps to Reproduce

The core formatting behavior has this minimal reproduction using public types:

var operation = Operation.builder()
        .id("callback")
        .type(OperationType.CALLBACK)
        .status(OperationStatus.FAILED)
        .callbackDetails(CallbackDetails.builder()
                .error(ErrorObject.builder()
                        .errorType("RejectedError")
                        .build())
                .build())
        .build();

var failure = new CallbackFailedException(operation);
System.out.println(failure.getMessage()); // RejectedError: null

For the full SDK-to-telemetry path:

  1. Register DurableExecutionTelemetry.durableExecutionTelemetry(...) with a capturing exporter and error details enabled.
  2. Run a named wait-for-callback and complete its callback with failure carrying only ErrorType.
  3. Allow the callback failure to escape the handler. Inspect the top-level exported error.message, and any containing operation error that was checkpointed.
  4. Also test a caught failure followed by a durable wait and resume, checking the stored operation error and exported record without repeating completed user work.

SDK Version

Source inspected at Java commit 4b1aa82b0d898fea495cda45760ce3737fbb341e. Exact deployed core/exporter JAR versions for the supplied observation were not recorded.

Java Version

Not recorded for the supplied cloud observation. The source defect is ordinary null String concatenation.

Is this a regression?

Unknown; no last-known-good version was established.

Source Evidence

Acceptance Criteria

  • Type-only callback failures do not append : null; message-only and type-plus-message failures preserve their supplied text.
  • Cover absent Error, empty ErrorObject, absent/empty type, and absent/empty message without introducing a new exception or changing failure classification.
  • Preserve the underlying stored ErrorObject and its absent fields; define the caller's type-only display message explicitly.
  • Add focused coverage through the real SDK and telemetry plugin for uncaught and caught/resumed failures. Check root and applicable operation error records, including the name-keyed format when a single operation represents that name.
  • Preserve replayed results, callback identity and side-effect counts.
  • Do not broaden this fix into the separate OTel operation exception-type issue [Bug]: OTel operation spans report DurableOperationException instead of the original error type #847.

Source inspection confirms the formatting and telemetry fallback paths. No new cloud deployment or raw backend retrieval was performed for filing this issue. Related #773 fixed the absent-Error NullPointerException; it does not cover the type-only : null message.

Activity

  1. added
    bugSomething isn't working
    pkg:pluginsPlugin interface, possibly impacting all plugins
    on Oct 30, 2026
  2. hln33 commented on Oct 30, 2026

    @hln33
    Contributor

    Severity - low

    What is wrong. CallbackFailedException.getMessage() returns RejectedError: null when an external system fails a callback with ErrorType and no ErrorMessage. The telemetry exporter falls back to that string for error.message, and the OTel plugins put it in the exception entries on the Workflow and Invocation spans. A child context that checkpoints the escaped exception stores the malformed text as the operation's ErrorMessage.

    What is correct. The error type is preserved everywhere: error.name in telemetry records is RejectedError, and the exception class is still CallbackFailedException. Failure classification, retries, replay, callback identity, and the stored ErrorObject for the callback itself are unchanged. Failures that carry an ErrorMessage, or carry neither field, format correctly.

    Scope. Only callbacks failed externally with a type-only error payload. Both fields are optional in SendDurableExecutionCallbackFailure, but most senders include a message.

    Not a regression. buildMessage has had this shape since the callback implementation landed on 2026-01-28, before v2.1.0. Every release since carries it. #773 fixed the adjacent absent-Error NPE and did not cover this case.

    Why not higher severity. No data loss, no outage, no change to execution behavior. The type is intact, so a customer can still identify the failure. The only persisted damage is a message string in a containing operation's checkpoint.

    Why not lower severity. The text null reaches customers through a public getMessage() contract and through two exporters, and it can be written into execution history. The fix is a null check in one method, so the cost of leaving it is higher than the cost of fixing it. The acceptance criteria are already written; this is a good first issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpkg:pluginsPlugin interface, possibly impacting all pluginspkg:sdkModule: sdk

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions